From aa285f3f7011c122d3c078e697e4e30e23c69739 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 17 May 2022 23:49:12 -0700 Subject: [PATCH 001/326] [Feature] Update Material Components Android. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1c8cf9f59..c86454394 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -120,7 +120,7 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.6.0-rc01' + implementation 'com.google.android.material:material:1.6.0' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' From 9d205ad1cb84bf6b21fc7eac8fbbbe74086cf08f Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 29 May 2022 13:30:09 -0700 Subject: [PATCH 002/326] [Feature] Update to AGP 7.2.1. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a7edcf84a..2a55da3c8 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.0' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 697b6fe63464c63ec70b144079ebd67be973816b Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 29 May 2022 19:34:51 -0700 Subject: [PATCH 003/326] [Feature] Update Coil to 2.1.0 and migrate. Also fetch video thumbnail at 1/3 of total duration. Fixes: #557 --- app/build.gradle | 2 +- ...> AppIconApplicationInfoFetcherFactory.kt} | 14 +- .../android/files/coil/AppIconFetcher.kt | 44 ++--- ...kt => AppIconPackageNameFetcherFactory.kt} | 12 +- .../android/files/coil/CoilExtensions.kt | 4 +- .../android/files/coil/CoilInitializer.kt | 17 +- .../zhanghai/android/files/coil/CoilUtils.kt | 33 ++++ .../files/coil/PathAttributesFetcher.kt | 150 ++++++++--------- .../android/files/coil/PdfPageFetcher.kt | 117 +++++++------- .../android/files/coil/VideoFrameFetcher.kt | 153 ++++++++++++++++++ .../filejob/FileJobConflictDialogFragment.kt | 12 +- .../android/files/filelist/FileListAdapter.kt | 12 +- .../permissions/PrincipalListAdapter.kt | 6 +- .../files/viewer/image/ImageViewerAdapter.kt | 14 +- 14 files changed, 396 insertions(+), 194 deletions(-) rename app/src/main/java/me/zhanghai/android/files/coil/{AppIconApplicationInfoFetcher.kt => AppIconApplicationInfoFetcherFactory.kt} (70%) rename app/src/main/java/me/zhanghai/android/files/coil/{AppIconPackageNameFetcher.kt => AppIconPackageNameFetcherFactory.kt} (78%) create mode 100644 app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt diff --git a/app/build.gradle b/app/build.gradle index c86454394..4fc4f86fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -165,7 +165,7 @@ dependencies { implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' implementation 'dev.rikka.shizuku:api:12.0.0' implementation 'eu.agno3.jcifs:jcifs-ng:2.1.6' - def coil_version = '1.4.0' + def coil_version = '2.1.0' implementation "io.coil-kt:coil:$coil_version" implementation "io.coil-kt:coil-gif:$coil_version" implementation "io.coil-kt:coil-svg:$coil_version" diff --git a/app/src/main/java/me/zhanghai/android/files/coil/AppIconApplicationInfoFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/AppIconApplicationInfoFetcherFactory.kt similarity index 70% rename from app/src/main/java/me/zhanghai/android/files/coil/AppIconApplicationInfoFetcher.kt rename to app/src/main/java/me/zhanghai/android/files/coil/AppIconApplicationInfoFetcherFactory.kt index acac50cfa..f9f804a27 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/AppIconApplicationInfoFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/AppIconApplicationInfoFetcherFactory.kt @@ -7,19 +7,25 @@ package me.zhanghai.android.files.coil import android.content.Context import android.content.pm.ApplicationInfo +import coil.key.Keyer +import coil.request.Options import me.zhanghai.android.appiconloader.AppIconLoader import me.zhanghai.android.files.R import me.zhanghai.android.files.compat.longVersionCodeCompat import me.zhanghai.android.files.util.getDimensionPixelSize import java.io.Closeable -class AppIconApplicationInfoFetcher(private val context: Context) : AppIconFetcher( +class AppIconApplicationInfoKeyer : Keyer { + override fun key(data: ApplicationInfo, options: Options): String = + AppIconLoader.getIconKey(data, data.longVersionCodeCompat, options.context) +} + +class AppIconApplicationInfoFetcherFactory( + context: Context +) : AppIconFetcher.Factory( // This is used by PrincipalListAdapter. context.getDimensionPixelSize(R.dimen.icon_size), context ) { - override fun key(data: ApplicationInfo): String? = - AppIconLoader.getIconKey(data, data.longVersionCodeCompat, context) - override fun getApplicationInfo(data: ApplicationInfo): Pair = data to null } diff --git a/app/src/main/java/me/zhanghai/android/files/coil/AppIconFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/AppIconFetcher.kt index 118463708..7a8c8c920 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/AppIconFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/AppIconFetcher.kt @@ -8,34 +8,38 @@ package me.zhanghai.android.files.coil import android.content.Context import android.content.pm.ApplicationInfo import androidx.core.graphics.drawable.toDrawable -import coil.bitmap.BitmapPool +import coil.ImageLoader import coil.decode.DataSource -import coil.decode.Options import coil.fetch.DrawableResult import coil.fetch.FetchResult import coil.fetch.Fetcher -import coil.size.Size +import coil.request.Options import me.zhanghai.android.appiconloader.AppIconLoader import java.io.Closeable -abstract class AppIconFetcher( - iconSize: Int, - private val context: Context, - shrinkNonAdaptiveIcons: Boolean = false -) : Fetcher { - private val appIconLoader = AppIconLoader(iconSize, shrinkNonAdaptiveIcons, context) - - abstract fun getApplicationInfo(data: T): Pair - - override suspend fun fetch( - pool: BitmapPool, - data: T, - size: Size, - options: Options - ): FetchResult { - val (applicationInfo, closeable) = getApplicationInfo(data) +class AppIconFetcher( + private val options: Options, + private val appIconLoader: AppIconLoader, + private val getApplicationInfo: () -> Pair +) : Fetcher { + override suspend fun fetch(): FetchResult { + val (applicationInfo, closeable) = getApplicationInfo() val icon = closeable.use { appIconLoader.loadIcon(applicationInfo) } // Not sampled because we only load with one fixed size. - return DrawableResult(icon.toDrawable(context.resources), false, DataSource.DISK) + return DrawableResult(icon.toDrawable(options.context.resources), false, DataSource.DISK) + } + + abstract class Factory( + iconSize: Int, + context: Context, + shrinkNonAdaptiveIcons: Boolean = false + ) : Fetcher.Factory { + private val appIconLoader = + AppIconLoader(iconSize, shrinkNonAdaptiveIcons, context) + + override fun create(data: T, options: Options, imageLoader: ImageLoader): Fetcher = + AppIconFetcher(options, appIconLoader) { getApplicationInfo(data) } + + abstract fun getApplicationInfo(data: T): Pair } } diff --git a/app/src/main/java/me/zhanghai/android/files/coil/AppIconPackageNameFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/AppIconPackageNameFetcherFactory.kt similarity index 78% rename from app/src/main/java/me/zhanghai/android/files/coil/AppIconPackageNameFetcher.kt rename to app/src/main/java/me/zhanghai/android/files/coil/AppIconPackageNameFetcherFactory.kt index 669a9405c..4e8977660 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/AppIconPackageNameFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/AppIconPackageNameFetcherFactory.kt @@ -7,6 +7,8 @@ package me.zhanghai.android.files.coil import android.content.Context import android.content.pm.ApplicationInfo +import coil.key.Keyer +import coil.request.Options import me.zhanghai.android.files.R import me.zhanghai.android.files.compat.PackageManagerCompat import me.zhanghai.android.files.util.getDimensionPixelSize @@ -14,13 +16,17 @@ import java.io.Closeable data class AppIconPackageName(val packageName: String) -class AppIconPackageNameFetcher(private val context: Context) : AppIconFetcher( +class AppIconPackageNameKeyer : Keyer { + override fun key(data: AppIconPackageName, options: Options): String = data.packageName +} + +class AppIconPackageNameFetcherFactory( + private val context: Context +) : AppIconFetcher.Factory( // This is used by FileListAdapter, and shrinking non-adaptive icons makes it look better as a // badge. context.getDimensionPixelSize(R.dimen.badge_size_plus_1dp), context, true ) { - override fun key(data: AppIconPackageName): String? = data.packageName - override fun getApplicationInfo(data: AppIconPackageName): Pair { // PackageManager.MATCH_UNINSTALLED_PACKAGES allows using PackageManager.MATCH_ANY_USER // without the INTERACT_ACROSS_USERS permission when we are in the system user and it has a diff --git a/app/src/main/java/me/zhanghai/android/files/coil/CoilExtensions.kt b/app/src/main/java/me/zhanghai/android/files/coil/CoilExtensions.kt index c1803f199..90ca73813 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/CoilExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/CoilExtensions.kt @@ -5,13 +5,11 @@ package me.zhanghai.android.files.coil -import coil.annotation.ExperimentalCoilApi import coil.request.ImageRequest import coil.transition.CrossfadeTransition -@OptIn(ExperimentalCoilApi::class) fun ImageRequest.Builder.fadeIn(durationMillis: Int): ImageRequest.Builder = apply { placeholder(android.R.color.transparent) - transition(CrossfadeTransition(durationMillis, true)) + transitionFactory(CrossfadeTransition.Factory(durationMillis, true)) } diff --git a/app/src/main/java/me/zhanghai/android/files/coil/CoilInitializer.kt b/app/src/main/java/me/zhanghai/android/files/coil/CoilInitializer.kt index 4d81b95e5..4072b3b06 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/CoilInitializer.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/CoilInitializer.kt @@ -16,18 +16,21 @@ import me.zhanghai.android.files.app.application fun initializeCoil() { Coil.setImageLoader( ImageLoader.Builder(application) - .componentRegistry { - add(AppIconApplicationInfoFetcher(application)) - add(AppIconPackageNameFetcher(application)) - add(PathAttributesFetcher(application)) + .components { + add(AppIconApplicationInfoKeyer()) + add(AppIconApplicationInfoFetcherFactory(application)) + add(AppIconPackageNameKeyer()) + add(AppIconPackageNameFetcherFactory(application)) + add(PathAttributesKeyer()) + add(PathAttributesFetcher.Factory(application)) add( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - ImageDecoderDecoder(application) + ImageDecoderDecoder.Factory() } else { - GifDecoder() + GifDecoder.Factory() } ) - add(SvgDecoder(application, false)) + add(SvgDecoder.Factory(false)) } .build() ) diff --git a/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt b/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt new file mode 100644 index 000000000..4c67688ae --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.coil + +import android.graphics.Bitmap +import android.os.Build +import coil.size.Dimension +import coil.size.Scale +import coil.size.Size +import coil.size.isOriginal +import coil.size.pxOrElse + +val Bitmap.Config.isHardware: Boolean + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this == Bitmap.Config.HARDWARE + +fun Bitmap.Config.toSoftware(): Bitmap.Config = if (isHardware) Bitmap.Config.ARGB_8888 else this + +inline fun Size.widthPx(scale: Scale, original: () -> Int): Int = + if (isOriginal) original() else width.toPx(scale) + +inline fun Size.heightPx(scale: Scale, original: () -> Int): Int = + if (isOriginal) original() else height.toPx(scale) + +fun Dimension.toPx(scale: Scale) = + pxOrElse { + when (scale) { + Scale.FILL -> Int.MIN_VALUE + Scale.FIT -> Int.MAX_VALUE + } + } diff --git a/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt index ea2e7f889..a4ebbbdbd 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt @@ -10,16 +10,16 @@ import android.content.pm.ApplicationInfo import android.media.MediaMetadataRetriever import android.os.ParcelFileDescriptor import androidx.core.graphics.drawable.toDrawable -import coil.bitmap.BitmapPool +import coil.ImageLoader import coil.decode.DataSource -import coil.decode.Options +import coil.decode.ImageSource import coil.fetch.DrawableResult import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.fetch.SourceResult -import coil.fetch.VideoFrameFetcher -import coil.size.PixelSize -import coil.size.Size +import coil.key.Keyer +import coil.request.Options +import coil.size.Dimension import java8.nio.file.Path import java8.nio.file.attribute.BasicFileAttributes import me.zhanghai.android.files.R @@ -53,75 +53,35 @@ import java.io.Closeable import java.io.IOException import me.zhanghai.android.files.util.setDataSource as appSetDataSource -class PathAttributesFetcher( - private val context: Context -) : Fetcher> { - private val appIconFetcher = object : AppIconFetcher( - // This is used by FileListAdapter. - context.getDimensionPixelSize(R.dimen.large_icon_size), context - ) { - override fun key(data: Path): String? { - throw AssertionError(data) - } - - override fun getApplicationInfo(data: Path): Pair { - val (packageInfo, closeable) = - context.packageManager.getPackageArchiveInfoCompat(data, 0) - val applicationInfo = packageInfo?.applicationInfo - if (applicationInfo == null) { - closeable?.close() - throw IOException("ApplicationInfo is null") - } - return applicationInfo to closeable - } - } - - private val videoFrameFetcher = object : VideoFrameFetcher(context) { - override fun key(data: Path): String? { - throw AssertionError(data) - } - - override fun MediaMetadataRetriever.setDataSource(data: Path) { - appSetDataSource(data) - } - } - - private val pdfPageFetcher = object : PdfPageFetcher(context) { - override fun key(data: Path): String? { - throw AssertionError(data) - } - - override fun openParcelFileDescriptor(data: Path): ParcelFileDescriptor = - when { - data.isLinuxPath -> - ParcelFileDescriptor.open(data.toFile(), ParcelFileDescriptor.MODE_READ_ONLY) - data.isDocumentPath -> - DocumentResolver.openParcelFileDescriptor(data as DocumentResolver.Path, "r") - else -> throw IllegalArgumentException(data.toString()) - } - } - - override fun key(data: Pair): String { +class PathAttributesKeyer : Keyer> { + override fun key(data: Pair, options: Options): String { val (path, attributes) = data return "$path:${attributes.lastModifiedInstant.toEpochMilli()}" } +} - override suspend fun fetch( - pool: BitmapPool, - data: Pair, - size: Size, - options: Options - ): FetchResult { +class PathAttributesFetcher( + private val data: Pair, + private val options: Options, + private val imageLoader: ImageLoader, + private val appIconFetcherFactory: AppIconFetcher.Factory, + private val videoFrameFetcherFactory: VideoFrameFetcher.Factory, + private val pdfPageFetcherFactory: PdfPageFetcher.Factory +) : Fetcher { + override suspend fun fetch(): FetchResult? { val (path, attributes) = data + val (width, height) = options.size // @see android.provider.MediaStore.ThumbnailConstants.MINI_SIZE - val isThumbnail = size is PixelSize && size.width <= 512 && size.height <= 384 + val isThumbnail = width is Dimension.Pixels && width.px <= 512 + && height is Dimension.Pixels && height.px <= 384 if (isThumbnail) { + width as Dimension.Pixels + height as Dimension.Pixels if (path.isDocumentPath && attributes.documentSupportsThumbnail) { - size as PixelSize val thumbnail = runWithCancellationSignal { signal -> try { DocumentResolver.getThumbnail( - path as DocumentResolver.Path, size.width, size.height, signal + path as DocumentResolver.Path, width.px, height.px, signal ) } catch (e: ResolverException) { e.printStackTrace() @@ -130,7 +90,7 @@ class PathAttributesFetcher( } if (thumbnail != null) { return DrawableResult( - thumbnail.toDrawable(context.resources), true, DataSource.DISK + thumbnail.toDrawable(options.context.resources), true, DataSource.DISK ) } } @@ -147,7 +107,7 @@ class PathAttributesFetcher( when { mimeType.isApk && path.isGetPackageArchiveInfoCompatible -> { try { - return appIconFetcher.fetch(pool, path, size, options) + return appIconFetcherFactory.create(path, options, imageLoader).fetch() } catch (e: Exception) { e.printStackTrace() } @@ -155,9 +115,8 @@ class PathAttributesFetcher( mimeType.isImage || mimeType == MimeType.GENERIC -> { val inputStream = path.newInputStream() return SourceResult( - inputStream.source().buffer(), - if (mimeType != MimeType.GENERIC) mimeType.value else null, - DataSource.DISK + ImageSource(inputStream.source().buffer(), options.context), + if (mimeType != MimeType.GENERIC) mimeType.value else null, DataSource.DISK ) } mimeType.isMedia && (path.isLinuxPath || path.isDocumentPath) -> { @@ -172,12 +131,14 @@ class PathAttributesFetcher( } if (embeddedPicture != null) { return SourceResult( - embeddedPicture.inputStream().source().buffer(), null, DataSource.DISK + ImageSource( + embeddedPicture.inputStream().source().buffer(), options.context + ), null, DataSource.DISK ) } if (mimeType.isVideo) { try { - return videoFrameFetcher.fetch(pool, path, size, options) + return videoFrameFetcherFactory.create(path, options, imageLoader).fetch() } catch (e: Exception) { e.printStackTrace() } @@ -185,12 +146,57 @@ class PathAttributesFetcher( } mimeType.isPdf && (path.isLinuxPath || path.isDocumentPath) -> { try { - return pdfPageFetcher.fetch(pool, path, size, options) + return pdfPageFetcherFactory.create(path, options, imageLoader).fetch() } catch (e: Exception) { e.printStackTrace() } } } - error("Cannot fetch $path") + return null + } + + class Factory(private val context: Context) : Fetcher.Factory> { + private val appIconFetcherFactory = object : AppIconFetcher.Factory( + // This is used by FileListAdapter. + context.getDimensionPixelSize(R.dimen.large_icon_size), context + ) { + override fun getApplicationInfo(data: Path): Pair { + val (packageInfo, closeable) = + context.packageManager.getPackageArchiveInfoCompat(data, 0) + val applicationInfo = packageInfo?.applicationInfo + if (applicationInfo == null) { + closeable?.close() + throw IOException("ApplicationInfo is null") + } + return applicationInfo to closeable + } + } + + private val videoFrameFetcherFactory = object : VideoFrameFetcher.Factory() { + override fun MediaMetadataRetriever.setDataSource(data: Path) { + appSetDataSource(data) + } + } + + private val pdfPageFetcherFactory = object : PdfPageFetcher.Factory() { + override fun openParcelFileDescriptor(data: Path): ParcelFileDescriptor = + when { + data.isLinuxPath -> + ParcelFileDescriptor.open(data.toFile(), ParcelFileDescriptor.MODE_READ_ONLY) + data.isDocumentPath -> + DocumentResolver.openParcelFileDescriptor(data as DocumentResolver.Path, "r") + else -> throw IllegalArgumentException(data.toString()) + } + } + + override fun create( + data: Pair, + options: Options, + imageLoader: ImageLoader + ): Fetcher = + PathAttributesFetcher( + data, options, imageLoader, appIconFetcherFactory, videoFrameFetcherFactory, + pdfPageFetcherFactory + ) } } diff --git a/app/src/main/java/me/zhanghai/android/files/coil/PdfPageFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/PdfPageFetcher.kt index 6fb1859ec..253d68c8a 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/PdfPageFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/PdfPageFetcher.kt @@ -5,31 +5,26 @@ package me.zhanghai.android.files.coil -import android.content.Context -import android.graphics.Bitmap import android.graphics.Color import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor import androidx.annotation.ColorInt import androidx.annotation.IntRange +import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.toDrawable -import coil.bitmap.BitmapPool +import coil.ImageLoader import coil.decode.DataSource import coil.decode.DecodeUtils -import coil.decode.Options import coil.fetch.DrawableResult import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.request.ImageRequest +import coil.request.Options import coil.request.Parameters -import coil.size.OriginalSize -import coil.size.PixelSize -import coil.size.Size import kotlin.math.roundToInt -fun ImageRequest.Builder.pdfBackgroundColor(@ColorInt backgroundColor: Int): ImageRequest.Builder { - return setParameter(PdfPageFetcher.PDF_BACKGROUND_COLOR_KEY, backgroundColor) -} +fun ImageRequest.Builder.pdfBackgroundColor(@ColorInt backgroundColor: Int): ImageRequest.Builder = + setParameter(PdfPageFetcher.PDF_BACKGROUND_COLOR_KEY, backgroundColor) fun ImageRequest.Builder.pdfPageIndex(@IntRange(from = 0) pageIndex: Int): ImageRequest.Builder { require(pageIndex >= 0) { "pageIndex must be >= 0." } @@ -42,66 +37,62 @@ fun Parameters.pdfBackgroundColor(): Int? = value(PdfPageFetcher.PDF_BACKGROUND_ @IntRange(from = 0) fun Parameters.pdfPageIndex(): Int? = value(PdfPageFetcher.PDF_PAGE_INDEX_KEY) as Int? -abstract class PdfPageFetcher(private val context: Context) : Fetcher { - protected abstract fun openParcelFileDescriptor(data: T): ParcelFileDescriptor - - override suspend fun fetch( - pool: BitmapPool, - data: T, - size: Size, - options: Options - ): FetchResult { - val pfd = openParcelFileDescriptor(data) - PdfRenderer(pfd).use { renderer -> - val pageIndex = options.parameters.pdfPageIndex() ?: 0 - renderer.openPage(pageIndex).use { page -> - val srcWidth = page.width - val srcHeight = page.height - val dstSize = when (size) { - is PixelSize -> { - if (srcWidth > 0 && srcHeight > 0) { - val rawScale = DecodeUtils.computeSizeMultiplier( - srcWidth = srcWidth, - srcHeight = srcHeight, - dstWidth = size.width, - dstHeight = size.height, - scale = options.scale - ) - val scale = if (options.allowInexactSize) { - rawScale.coerceAtMost(1.0) - } else { - rawScale - } - val width = (scale * srcWidth).roundToInt() - val height = (scale * srcHeight).roundToInt() - PixelSize(width, height) - } else { - OriginalSize - } +class PdfPageFetcher( + private val options: Options, + private val openParcelFileDescriptor: () -> ParcelFileDescriptor +) : Fetcher { + override suspend fun fetch(): FetchResult = + openParcelFileDescriptor().use { pfd -> + PdfRenderer(pfd).use { renderer -> + val pageIndex = options.parameters.pdfPageIndex() ?: 0 + renderer.openPage(pageIndex).use { page -> + val srcWidth = page.width + check(srcWidth > 0) { + "PDF page $pageIndex width $srcWidth isn't greater than 0" } - is OriginalSize -> OriginalSize - } - val bitmap = if (dstSize is PixelSize) { - pool.getDirty(dstSize.width, dstSize.height, Bitmap.Config.ARGB_8888) - } else { - pool.getDirty(srcWidth, srcHeight, Bitmap.Config.ARGB_8888) + val srcHeight = page.height + check(srcWidth > 0) { + "PDF page $pageIndex height $srcHeight isn't greater than 0" + } + val dstWidth = options.size.widthPx(options.scale) { srcWidth } + val dstHeight = options.size.heightPx(options.scale) { srcHeight } + val rawScale = DecodeUtils.computeSizeMultiplier( + srcWidth = srcWidth, + srcHeight = srcHeight, + dstWidth = dstWidth, + dstHeight = dstHeight, + scale = options.scale + ) + val scale = if (options.allowInexactSize) { + rawScale.coerceAtMost(1.0) + } else { + rawScale + } + val width = (scale * srcWidth).roundToInt() + val height = (scale * srcHeight).roundToInt() + val config = options.config.toSoftware() + val bitmap = createBitmap(width, height, config) + val backgroundColor = options.parameters.pdfBackgroundColor() ?: Color.WHITE + bitmap.eraseColor(backgroundColor) + page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + DrawableResult( + drawable = bitmap.toDrawable(options.context.resources), + isSampled = scale < 1.0, + dataSource = DataSource.DISK + ) } - val backgroundColor = options.parameters.pdfBackgroundColor() ?: Color.WHITE - bitmap.eraseColor(backgroundColor) - page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) - val isSampled = dstSize is PixelSize - && (dstSize.width < srcWidth || dstSize.height < srcHeight) - return DrawableResult( - drawable = bitmap.toDrawable(context.resources), - isSampled = isSampled, - dataSource = DataSource.DISK - ) } } - } companion object { const val PDF_BACKGROUND_COLOR_KEY = "coil#pdf_background_color" const val PDF_PAGE_INDEX_KEY = "coil#pdf_page_index" } + + abstract class Factory : Fetcher.Factory { + override fun create(data: T, options: Options, imageLoader: ImageLoader): Fetcher = + PdfPageFetcher(options) { openParcelFileDescriptor(data) } + + protected abstract fun openParcelFileDescriptor(data: T): ParcelFileDescriptor + } } diff --git a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt new file mode 100644 index 000000000..1dbc50056 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2022 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.coil + +import android.graphics.Paint +import android.media.MediaMetadataRetriever +import android.os.Build +import androidx.core.graphics.applyCanvas +import androidx.core.graphics.createBitmap +import androidx.core.graphics.drawable.toDrawable +import coil.ImageLoader +import coil.decode.DataSource +import coil.decode.DecodeUtils +import coil.fetch.DrawableResult +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.request.ImageRequest +import coil.request.Options +import coil.request.Parameters +import coil.request.videoFrameOption +import java.util.concurrent.TimeUnit +import kotlin.math.roundToInt +import kotlin.math.roundToLong + +fun ImageRequest.Builder.videoFrameFraction(frameFraction: Double): ImageRequest.Builder { + require(frameFraction in 0.0..1.0) { "frameFraction must be >= 0 and <= 1." } + return setParameter(VideoFrameFetcher.VIDEO_FRAME_FRACTION_KEY, frameFraction) +} + +fun Parameters.videoFrameFraction(): Double? = + value(VideoFrameFetcher.VIDEO_FRAME_FRACTION_KEY) as Double? + +class VideoFrameFetcher( + private val options: Options, + private val setDataSource: MediaMetadataRetriever.() -> Unit +) : Fetcher { + override suspend fun fetch(): FetchResult = + MediaMetadataRetriever().use { retriever -> + retriever.setDataSource() + val rotation = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) + ?.toIntOrNull() ?: 0 + var srcWidth: Int + var srcHeight: Int + when (rotation) { + 90, 270 -> { + srcWidth = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) + ?.toIntOrNull() ?: 0 + srcHeight = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) + ?.toIntOrNull() ?: 0 + } + else -> { + srcWidth = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) + ?.toIntOrNull() ?: 0 + srcHeight = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) + ?.toIntOrNull() ?: 0 + } + } + val durationMillis = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + ?.toLongOrNull() ?: 0L + // 1/3 is the first fraction tried by totem-video-thumbnailer. + // @see https://gitlab.gnome.org/GNOME/totem/-/blob/master/src/totem-video-thumbnailer.c#L543 + val frameFraction = options.parameters.videoFrameFraction() ?: (1.0 / 3.0) + val frameMicros = TimeUnit.MICROSECONDS.convert( + (frameFraction * durationMillis).roundToLong(), TimeUnit.MILLISECONDS + ) + val frameOption = options.parameters.videoFrameOption() + ?: MediaMetadataRetriever.OPTION_CLOSEST_SYNC + val outBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 + && srcWidth > 0 && srcHeight > 0) { + val dstWidth = options.size.widthPx(options.scale) { srcWidth } + val dstHeight = options.size.heightPx(options.scale) { srcHeight } + val rawScale = DecodeUtils.computeSizeMultiplier( + srcWidth = srcWidth, + srcHeight = srcHeight, + dstWidth = dstWidth, + dstHeight = dstHeight, + scale = options.scale + ) + val scale = if (options.allowInexactSize) { + rawScale.coerceAtMost(1.0) + } else { + rawScale + } + val width = (scale * srcWidth).roundToInt() + val height = (scale * srcHeight).roundToInt() + retriever.getScaledFrameAtTime(frameMicros, frameOption, width, height) + } else { + retriever.getFrameAtTime(frameMicros, frameOption)?.also { + srcWidth = it.width + srcHeight = it.height + } + } + val dstWidth = options.size.widthPx(options.scale) { srcWidth } + val dstHeight = options.size.heightPx(options.scale) { srcHeight } + val rawScale = DecodeUtils.computeSizeMultiplier( + srcWidth = srcWidth, + srcHeight = srcHeight, + dstWidth = dstWidth, + dstHeight = dstHeight, + scale = options.scale + ) + checkNotNull(outBitmap) { "Failed to decode frame at $frameMicros microseconds" } + val scale = if (options.allowInexactSize) { + rawScale.coerceAtMost(1.0) + } else { + rawScale + } + val width = (scale * srcWidth).roundToInt() + val height = (scale * srcHeight).roundToInt() + val isValidSize = if (options.allowInexactSize) { + outBitmap.width <= width && outBitmap.height <= height + } else { + outBitmap.width == width && outBitmap.height == height + } + val isValidConfig = !outBitmap.config.isHardware || options.config.isHardware + val bitmap = if (isValidSize && isValidConfig) { + outBitmap + } else { + val config = options.config.toSoftware() + createBitmap(width, height, config).applyCanvas { + scale(scale.toFloat(), scale.toFloat()) + val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG) + drawBitmap(outBitmap, 0f, 0f, paint) + outBitmap.recycle() + } + } + DrawableResult( + drawable = bitmap.toDrawable(options.context.resources), + isSampled = scale < 1.0, + dataSource = DataSource.DISK + ) + } + + companion object { + const val VIDEO_FRAME_FRACTION_KEY = "coil#video_frame_fraction" + } + + abstract class Factory : Fetcher.Factory { + override fun create(data: T, options: Options, imageLoader: ImageLoader): Fetcher = + VideoFrameFetcher(options) { setDataSource(data) } + + protected abstract fun MediaMetadataRetriever.setDataSource(data: T) + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt index 2d648c4f5..b41c121e8 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt @@ -21,8 +21,8 @@ import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView import androidx.core.widget.doAfterTextChanged import androidx.interpolator.view.animation.FastOutSlowInInterpolator -import coil.clear -import coil.loadAny +import coil.dispose +import coil.load import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.parcelize.Parceler import kotlinx.parcelize.Parcelize @@ -153,21 +153,21 @@ class FileJobConflictDialogFragment : AppCompatDialogFragment() { val path = file.path iconImage.setImageResource(file.mimeType.iconRes) iconImage.isVisible = true - thumbnailImage.clear() + thumbnailImage.dispose() thumbnailImage.setImageDrawable(null) val attributes = file.attributes if (file.supportsThumbnail) { - thumbnailImage.loadAny(path to attributes) { + thumbnailImage.load(path to attributes) { listener { _, _ -> iconImage.isVisible = false } } } - appIconBadgeImage.clear() + appIconBadgeImage.dispose() appIconBadgeImage.setImageDrawable(null) val appDirectoryPackageName = file.appDirectoryPackageName val hasAppIconBadge = appDirectoryPackageName != null appIconBadgeImage.isVisible = hasAppIconBadge if (hasAppIconBadge) { - appIconBadgeImage.loadAny(AppIconPackageName(appDirectoryPackageName!!)) + appIconBadgeImage.load(AppIconPackageName(appDirectoryPackageName!!)) } val badgeIconRes = if (file.attributesNoFollowLinks.isSymbolicLink) { if (file.isSymbolicLinkBroken) { diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt index 3f98f6d3c..8e8e2b5c3 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt @@ -11,8 +11,8 @@ import androidx.appcompat.widget.PopupMenu import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import coil.clear -import coil.loadAny +import coil.dispose +import coil.load import java8.nio.file.Path import me.zhanghai.android.fastscroll.PopupTextProvider import me.zhanghai.android.files.R @@ -199,23 +199,23 @@ class FileListAdapter( binding.iconLayout.setOnClickListener { selectFile(file) } binding.iconImage.setImageResource(file.mimeType.iconRes) binding.iconImage.isVisible = true - binding.thumbnailImage.clear() + binding.thumbnailImage.dispose() binding.thumbnailImage.setImageDrawable(null) val supportsThumbnail = file.supportsThumbnail binding.thumbnailImage.isVisible = supportsThumbnail val attributes = file.attributes if (supportsThumbnail) { - binding.thumbnailImage.loadAny(path to attributes) { + binding.thumbnailImage.load(path to attributes) { listener { _, _ -> binding.iconImage.isVisible = false } } } - binding.appIconBadgeImage.clear() + binding.appIconBadgeImage.dispose() binding.appIconBadgeImage.setImageDrawable(null) val appDirectoryPackageName = file.appDirectoryPackageName val hasAppIconBadge = appDirectoryPackageName != null binding.appIconBadgeImage.isVisible = hasAppIconBadge if (hasAppIconBadge) { - binding.appIconBadgeImage.loadAny(AppIconPackageName(appDirectoryPackageName!!)) + binding.appIconBadgeImage.load(AppIconPackageName(appDirectoryPackageName!!)) } val badgeIconRes = if (file.attributesNoFollowLinks.isSymbolicLink) { if (file.isSymbolicLinkBroken) { diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListAdapter.kt index c4d2da179..6ac0e008c 100644 --- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListAdapter.kt +++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListAdapter.kt @@ -9,6 +9,8 @@ import android.view.ViewGroup import androidx.annotation.DrawableRes import androidx.recyclerview.widget.RecyclerView import coil.clear +import coil.dispose +import coil.load import coil.loadAny import me.zhanghai.android.files.R import me.zhanghai.android.files.coil.ignoreError @@ -46,12 +48,12 @@ abstract class PrincipalListAdapter( val icon = binding.iconImage.context.getDrawableCompat(principalIconRes) val applicationInfo = principal.applicationInfos.firstOrNull() if (applicationInfo != null) { - binding.iconImage.loadAny(applicationInfo) { + binding.iconImage.load(applicationInfo) { placeholder(icon) ignoreError() } } else { - binding.iconImage.clear() + binding.iconImage.dispose() binding.iconImage.setImageDrawable(icon) } binding.principalText.text = if (principal.name != null) { diff --git a/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerAdapter.kt b/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerAdapter.kt index dcf35681c..89b900921 100644 --- a/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerAdapter.kt +++ b/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerAdapter.kt @@ -12,9 +12,9 @@ import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView -import coil.clear -import coil.loadAny -import coil.size.OriginalSize +import coil.dispose +import coil.load +import coil.size.Size import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.DefaultOnImageEventListener @@ -63,7 +63,7 @@ class ImageViewerAdapter( super.onViewRecycled(holder) val binding = holder.binding - binding.image.clear() + binding.image.dispose() binding.largeImage.recycle() } @@ -103,12 +103,12 @@ class ImageViewerAdapter( if (!imageInfo.shouldUseLargeImageView) { binding.image.apply { isVisible = true - loadAny(path to imageInfo.attributes) { - size(OriginalSize) + load(path to imageInfo.attributes) { + size(Size.ORIGINAL) fadeIn(context.shortAnimTime) listener( onSuccess = { _, _ -> binding.progress.fadeOutUnsafe() }, - onError = { _, e -> showError(binding, e) } + onError = { _, result -> showError(binding, result.throwable) } ) } } From c08487b9e7f069af4939002d60af5053cf85e367 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 1 Jun 2022 03:55:51 -0700 Subject: [PATCH 004/326] [Feature] Update Crashlytics Gradle plugin. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 4fc4f86fc..fbfacd55c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,7 +21,7 @@ buildscript { } dependencies { classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0' } } apply plugin: 'com.google.gms.google-services' From 75ffe055c5a263ea84d402a5fac2f3e21ac44a67 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 1 Jun 2022 03:56:07 -0700 Subject: [PATCH 005/326] [Feature] Build against and target API 32. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fbfacd55c..d32abf1e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,13 +30,13 @@ apply plugin: 'com.google.firebase.crashlytics' android { namespace 'me.zhanghai.android.files' - compileSdkVersion 31 + compileSdkVersion 32 ndkVersion '24.0.8215888' buildToolsVersion '32.0.0' defaultConfig { applicationId 'me.zhanghai.android.files' minSdkVersion 21 - targetSdkVersion 31 + targetSdkVersion 32 versionCode 31 versionName '1.5.2' resValue 'string', 'app_version', versionName + ' (' + versionCode + ')' From df3020d25fd20cb940fee09c711cc7a0303447e5 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 1 Jun 2022 14:58:09 -0700 Subject: [PATCH 006/326] [Feature] Update AndroidX libraries. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d32abf1e8..7215bc113 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -107,8 +107,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.fragment:fragment-ktx:1.4.1' def androidx_lifecycle_version = '2.4.1' From 6b031ba31e6fe908d80a37378cc017ec27530d73 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 2 Jun 2022 05:11:30 -0700 Subject: [PATCH 007/326] [Feature] Update libsu. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 7215bc113..5da3de33f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -95,7 +95,7 @@ repositories { dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' releaseImplementation 'com.github.mypplication:stetho-noop:1.1' - implementation 'com.github.topjohnwu.libsu:service:5.0.1' + implementation 'com.github.topjohnwu.libsu:service:5.0.2' } dependencies { From c2ceb2d1c1e36541dabbc8a693444ac5faecc37e Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 5 Jun 2022 22:59:07 -0700 Subject: [PATCH 008/326] [Fix] Fix kotlinc warning for using -opt-in instead of -Xopt-in. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 5da3de33f..27bb36ac5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,7 +54,7 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() freeCompilerArgs += [ - '-Xopt-in=kotlin.RequiresOptIn', + '-opt-in=kotlin.RequiresOptIn', ] } externalNativeBuild { From e7f4e91750a2e923038369f511f0ef62f3156db9 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 10 Jun 2022 23:27:25 -0700 Subject: [PATCH 009/326] [Feature] Update Kotlin to 1.7.0 and Material Components for Android to 1.6.1. --- app/build.gradle | 8 +------- build.gradle | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 27bb36ac5..f30c10afe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,12 +51,6 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - freeCompilerArgs += [ - '-opt-in=kotlin.RequiresOptIn', - ] - } externalNativeBuild { cmake { path 'CMakeLists.txt' @@ -120,7 +114,7 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.6.0' + implementation 'com.google.android.material:material:1.6.1' implementation 'com.caverock:androidsvg-aar:1.4' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' diff --git a/build.gradle b/build.gradle index 2a55da3c8..06450cfa9 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext { - kotlin_version = '1.6.21' + kotlin_version = '1.7.0' } repositories { google() From e8406f73c05a8e1b4be92e26f10d31b42a24578d Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 12 Jun 2022 04:12:19 -0700 Subject: [PATCH 010/326] [Refactor] Rename VideoFrameFetcher API to upstreamed version. --- .../android/files/coil/VideoFrameFetcher.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt index 1dbc50056..83aceaf06 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt @@ -25,13 +25,13 @@ import java.util.concurrent.TimeUnit import kotlin.math.roundToInt import kotlin.math.roundToLong -fun ImageRequest.Builder.videoFrameFraction(frameFraction: Double): ImageRequest.Builder { - require(frameFraction in 0.0..1.0) { "frameFraction must be >= 0 and <= 1." } - return setParameter(VideoFrameFetcher.VIDEO_FRAME_FRACTION_KEY, frameFraction) +fun ImageRequest.Builder.videoFramePercent(framePercent: Double): ImageRequest.Builder { + require(framePercent in 0.0..1.0) { "framePercent must be >= 0 and <= 1." } + return setParameter(VideoFrameFetcher.VIDEO_FRAME_PERCENT_KEY, framePercent) } -fun Parameters.videoFrameFraction(): Double? = - value(VideoFrameFetcher.VIDEO_FRAME_FRACTION_KEY) as Double? +fun Parameters.videoFramePercent(): Double? = + value(VideoFrameFetcher.VIDEO_FRAME_PERCENT_KEY) as Double? class VideoFrameFetcher( private val options: Options, @@ -66,11 +66,11 @@ class VideoFrameFetcher( val durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) ?.toLongOrNull() ?: 0L - // 1/3 is the first fraction tried by totem-video-thumbnailer. + // 1/3 is the first percentage tried by totem-video-thumbnailer. // @see https://gitlab.gnome.org/GNOME/totem/-/blob/master/src/totem-video-thumbnailer.c#L543 - val frameFraction = options.parameters.videoFrameFraction() ?: (1.0 / 3.0) + val framePercent = options.parameters.videoFramePercent() ?: (1.0 / 3.0) val frameMicros = TimeUnit.MICROSECONDS.convert( - (frameFraction * durationMillis).roundToLong(), TimeUnit.MILLISECONDS + (framePercent * durationMillis).roundToLong(), TimeUnit.MILLISECONDS ) val frameOption = options.parameters.videoFrameOption() ?: MediaMetadataRetriever.OPTION_CLOSEST_SYNC @@ -141,7 +141,7 @@ class VideoFrameFetcher( } companion object { - const val VIDEO_FRAME_FRACTION_KEY = "coil#video_frame_fraction" + const val VIDEO_FRAME_PERCENT_KEY = "coil#video_frame_percent" } abstract class Factory : Fetcher.Factory { From 29e7002acdc5a8d5e1b486483b5613ebc278ae09 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 30 Jun 2022 02:40:31 -0700 Subject: [PATCH 011/326] [Feature] Update dependencies. --- app/build.gradle | 4 ++-- .../me/zhanghai/android/files/util/FragmentViewModelLazy.kt | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f30c10afe..637b296f7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -104,8 +104,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.exifinterface:exifinterface:1.3.3' - implementation 'androidx.fragment:fragment-ktx:1.4.1' - def androidx_lifecycle_version = '2.4.1' + implementation 'androidx.fragment:fragment-ktx:1.5.0' + def androidx_lifecycle_version = '2.5.0' implementation "androidx.lifecycle:lifecycle-common-java8:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-process:$androidx_lifecycle_version" diff --git a/app/src/main/java/me/zhanghai/android/files/util/FragmentViewModelLazy.kt b/app/src/main/java/me/zhanghai/android/files/util/FragmentViewModelLazy.kt index bd25e0469..ce6199e9a 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/FragmentViewModelLazy.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/FragmentViewModelLazy.kt @@ -10,12 +10,15 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.viewmodel.CreationExtras inline fun Fragment.viewModels( noinline ownerProducer: () -> ViewModelStoreOwner = { this }, + noinline extrasProducer: (() -> CreationExtras)? = null, noinline factoryProducer: (() -> () -> VM)? = null ) = viewModels( ownerProducer, + extrasProducer, factoryProducer?.let { { val factory = it() @@ -28,5 +31,6 @@ inline fun Fragment.viewModels( ) inline fun Fragment.activityViewModels( + noinline extrasProducer: (() -> CreationExtras)? = null, noinline factoryProducer: (() -> () -> VM)? = null -) = viewModels(::requireActivity, factoryProducer) +) = viewModels(::requireActivity, extrasProducer, factoryProducer) From 590c8ea7f2c758a352752e830beeb2ec1abac5bb Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 6 Jul 2022 21:44:09 -0700 Subject: [PATCH 012/326] [Fix] Fix crash in toEnumSet() when set is empty. Fixes: #825 --- .../me/zhanghai/android/files/util/CollectionExtensions.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/zhanghai/android/files/util/CollectionExtensions.kt b/app/src/main/java/me/zhanghai/android/files/util/CollectionExtensions.kt index fe2147d55..3752bbe57 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/CollectionExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/CollectionExtensions.kt @@ -31,7 +31,8 @@ fun > enumSetOf(first: T, vararg rest: T): EnumSet = EnumSet.of(f fun Iterable.toLinkedSet(): LinkedHashSet = toCollection(LinkedHashSet()) -fun > Collection.toEnumSet(): EnumSet = EnumSet.copyOf(this) +inline fun > Collection.toEnumSet(): EnumSet = + if (isNotEmpty()) EnumSet.copyOf(this) else EnumSet.noneOf(T::class.java) fun > T.takeIfNotEmpty(): T? = if (isNotEmpty()) this else null From e2df8651efa5a5c5e1a2bfc31928f989f6f45c1f Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 9 Jul 2022 20:04:16 -0700 Subject: [PATCH 013/326] [Feature] Update Kotlin. --- app/build.gradle | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 637b296f7..12e26c89c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,7 +97,7 @@ dependencies { // kotlinx-coroutines-android depends on kotlin-stdlib-jdk8 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - def kotlinx_coroutines_version = '1.6.0' + def kotlinx_coroutines_version = '1.6.3' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" diff --git a/build.gradle b/build.gradle index 06450cfa9..8b4c422e1 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext { - kotlin_version = '1.7.0' + kotlin_version = '1.7.10' } repositories { google() From 596ff3d5d00b8b67f6cd3ae24677d4bfb7ad80e4 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 20 Aug 2022 21:48:01 -0700 Subject: [PATCH 014/326] [Feature] Update dependencies. --- app/build.gradle | 8 ++++---- build.gradle | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 12e26c89c..bf8f6c864 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -97,15 +97,15 @@ dependencies { // kotlinx-coroutines-android depends on kotlin-stdlib-jdk8 implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - def kotlinx_coroutines_version = '1.6.3' + def kotlinx_coroutines_version = '1.6.4' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" - implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.appcompat:appcompat:1.5.0' implementation 'androidx.core:core-ktx:1.8.0' implementation 'androidx.exifinterface:exifinterface:1.3.3' - implementation 'androidx.fragment:fragment-ktx:1.5.0' - def androidx_lifecycle_version = '2.5.0' + implementation 'androidx.fragment:fragment-ktx:1.5.2' + def androidx_lifecycle_version = '2.5.1' implementation "androidx.lifecycle:lifecycle-common-java8:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-process:$androidx_lifecycle_version" diff --git a/build.gradle b/build.gradle index 8b4c422e1..eb9c4a782 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 95c5b7c1ca865e3ced87f53d643c7e26f6cfaf76 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 20 Aug 2022 21:48:15 -0700 Subject: [PATCH 015/326] [Fix] Use app:drawableStartCompat instead of android:drawableStart. --- app/src/main/res/layout/set_principal_dialog.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/set_principal_dialog.xml b/app/src/main/res/layout/set_principal_dialog.xml index 69e4f0057..1f5bc5d61 100644 --- a/app/src/main/res/layout/set_principal_dialog.xml +++ b/app/src/main/res/layout/set_principal_dialog.xml @@ -7,6 +7,7 @@ Date: Mon, 29 Aug 2022 15:00:55 -0700 Subject: [PATCH 016/326] [Feature] Update Coil to 2.2.0 and switch to official videoFramePercent(). --- app/build.gradle | 2 +- .../android/files/coil/VideoFrameFetcher.kt | 15 +-------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bf8f6c864..bd0b6e881 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -159,7 +159,7 @@ dependencies { implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' implementation 'dev.rikka.shizuku:api:12.0.0' implementation 'eu.agno3.jcifs:jcifs-ng:2.1.6' - def coil_version = '2.1.0' + def coil_version = '2.2.0' implementation "io.coil-kt:coil:$coil_version" implementation "io.coil-kt:coil-gif:$coil_version" implementation "io.coil-kt:coil-svg:$coil_version" diff --git a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt index 83aceaf06..cc0446129 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt @@ -17,22 +17,13 @@ import coil.decode.DecodeUtils import coil.fetch.DrawableResult import coil.fetch.FetchResult import coil.fetch.Fetcher -import coil.request.ImageRequest import coil.request.Options -import coil.request.Parameters import coil.request.videoFrameOption +import coil.request.videoFramePercent import java.util.concurrent.TimeUnit import kotlin.math.roundToInt import kotlin.math.roundToLong -fun ImageRequest.Builder.videoFramePercent(framePercent: Double): ImageRequest.Builder { - require(framePercent in 0.0..1.0) { "framePercent must be >= 0 and <= 1." } - return setParameter(VideoFrameFetcher.VIDEO_FRAME_PERCENT_KEY, framePercent) -} - -fun Parameters.videoFramePercent(): Double? = - value(VideoFrameFetcher.VIDEO_FRAME_PERCENT_KEY) as Double? - class VideoFrameFetcher( private val options: Options, private val setDataSource: MediaMetadataRetriever.() -> Unit @@ -140,10 +131,6 @@ class VideoFrameFetcher( ) } - companion object { - const val VIDEO_FRAME_PERCENT_KEY = "coil#video_frame_percent" - } - abstract class Factory : Fetcher.Factory { override fun create(data: T, options: Options, imageLoader: ImageLoader): Fetcher = VideoFrameFetcher(options) { setDataSource(data) } From 5376565eb4eef288580f75c6dc9800c0d40febd5 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 29 Aug 2022 15:06:34 -0700 Subject: [PATCH 017/326] [Fix] Fix video preview not working on older platform versions. --- .../java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt index cc0446129..9f92f9b49 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt @@ -23,6 +23,7 @@ import coil.request.videoFramePercent import java.util.concurrent.TimeUnit import kotlin.math.roundToInt import kotlin.math.roundToLong +import me.zhanghai.android.files.compat.use class VideoFrameFetcher( private val options: Options, From 2a09b47ba0e8f50e7467355ce06c8824882a9d7f Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 3 Sep 2022 14:26:00 -0700 Subject: [PATCH 018/326] [Feature] Update dependencies. --- app/build.gradle | 8 +++++--- .../android/files/viewer/text/TextEditorFragment.kt | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bd0b6e881..5def8ef92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,12 +30,14 @@ apply plugin: 'com.google.firebase.crashlytics' android { namespace 'me.zhanghai.android.files' - compileSdkVersion 32 - ndkVersion '24.0.8215888' - buildToolsVersion '32.0.0' + compileSdkVersion 33 + ndkVersion '25.1.8937393' + buildToolsVersion '33.0.0' defaultConfig { applicationId 'me.zhanghai.android.files' minSdkVersion 21 + // Not supporting notification runtime permission yet. + //noinspection OldTargetApi targetSdkVersion 32 versionCode 31 versionName '1.5.2' diff --git a/app/src/main/java/me/zhanghai/android/files/viewer/text/TextEditorFragment.kt b/app/src/main/java/me/zhanghai/android/files/viewer/text/TextEditorFragment.kt index 64651265d..1c5163dc2 100644 --- a/app/src/main/java/me/zhanghai/android/files/viewer/text/TextEditorFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/viewer/text/TextEditorFragment.kt @@ -272,7 +272,7 @@ class TextEditorFragment : Fragment(), ConfirmReloadDialogFragment.Listener, companion object { fun inflate(menu: Menu, inflater: MenuInflater): MenuBinding { inflater.inflate(R.menu.text_editor, menu) - val encodingSubMenu = menu.findItem(R.id.action_encoding).subMenu + val encodingSubMenu = menu.findItem(R.id.action_encoding).subMenu!! for ((charsetName, charset) in Charset.availableCharsets()) { // HACK: Use titleCondensed to store charset name. encodingSubMenu.add(Menu.NONE, Menu.FIRST, Menu.NONE, charset.displayName()) From d00dd3819690d4125b32dc0c8454c6055ff080b3 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 3 Sep 2022 14:31:13 -0700 Subject: [PATCH 019/326] [Feature] Update Firebase and disable Ad ID collection. --- app/build.gradle | 6 +++--- app/src/main/AndroidManifest.xml | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5def8ef92..82dc65523 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.0' + classpath 'com.google.gms:google-services:4.3.13' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1' } } apply plugin: 'com.google.gms.google-services' @@ -195,7 +195,7 @@ dependencies { //#ifdef NONFREE implementation 'com.github.junrar:junrar:7.4.1' - implementation platform('com.google.firebase:firebase-bom:29.0.3') + implementation platform('com.google.firebase:firebase-bom:30.4.0') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-crashlytics-ndk' //#endif diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 31abbca20..0c2cf8007 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,11 @@ android:maxSdkVersion="30" /> + + + @@ -334,5 +339,10 @@ + + + From 7fa218ca085be5d98f479463070a256ca31c7e17 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 3 Sep 2022 16:38:14 -0700 Subject: [PATCH 020/326] [Fix] Add back Ad ID collection because actually it's already being used. --- app/src/main/AndroidManifest.xml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c2cf8007..31abbca20 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,11 +28,6 @@ android:maxSdkVersion="30" /> - - - @@ -339,10 +334,5 @@ - - - From 8b85112de189212f2d3a7ae8282c722984a0a42d Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 3 Sep 2022 17:09:35 -0700 Subject: [PATCH 021/326] [Feature] Update AppIconLoader. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 82dc65523..adc56373b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -166,7 +166,7 @@ dependencies { implementation "io.coil-kt:coil-gif:$coil_version" implementation "io.coil-kt:coil-svg:$coil_version" implementation "io.coil-kt:coil-video:$coil_version" - implementation 'me.zhanghai.android.appiconloader:appiconloader:1.4.0' + implementation 'me.zhanghai.android.appiconloader:appiconloader:1.5.0' implementation 'me.zhanghai.android.fastscroll:library:1.1.8' implementation 'me.zhanghai.android.foregroundcompat:library:1.0.2' implementation 'me.zhanghai.android.libselinux:library:2.1.0' From da3b1e995aa8d4c3ab7bfaf46bc1b29595979980 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 7 Sep 2022 11:14:55 -0700 Subject: [PATCH 022/326] [Feature] Update dependencies. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index adc56373b..e4cc363e0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -103,8 +103,8 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" - implementation 'androidx.appcompat:appcompat:1.5.0' - implementation 'androidx.core:core-ktx:1.8.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.exifinterface:exifinterface:1.3.3' implementation 'androidx.fragment:fragment-ktx:1.5.2' def androidx_lifecycle_version = '2.5.1' From d490cac9e41187b5fcb74237c1da420de7ffbc7d Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 11 Sep 2022 17:07:14 -0700 Subject: [PATCH 023/326] [Feature] Update Coil. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e4cc363e0..cc66701fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,7 +161,7 @@ dependencies { implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' implementation 'dev.rikka.shizuku:api:12.0.0' implementation 'eu.agno3.jcifs:jcifs-ng:2.1.6' - def coil_version = '2.2.0' + def coil_version = '2.2.1' implementation "io.coil-kt:coil:$coil_version" implementation "io.coil-kt:coil-gif:$coil_version" implementation "io.coil-kt:coil-svg:$coil_version" From 8b7934173ae94c15ca6520cd6458a5f87e53d252 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 11 Sep 2022 17:31:23 -0700 Subject: [PATCH 024/326] [Fix] Fix color swatch view on API 21-22. --- app/src/main/res/drawable/color_swatch_view_background.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/res/drawable/color_swatch_view_background.xml b/app/src/main/res/drawable/color_swatch_view_background.xml index ed14e8417..a73ae1668 100644 --- a/app/src/main/res/drawable/color_swatch_view_background.xml +++ b/app/src/main/res/drawable/color_swatch_view_background.xml @@ -23,7 +23,14 @@ --> + + From ef6fd9aedc9d886c9b90932d03321b2dcc56eecf Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 22 Sep 2022 02:32:11 -0700 Subject: [PATCH 025/326] [Feature] Update dependencies. --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cc66701fc..d9048f8f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.google.gms:google-services:4.3.13' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.1' + classpath 'com.google.gms:google-services:4.3.14' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2' } } apply plugin: 'com.google.gms.google-services' @@ -106,7 +106,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.exifinterface:exifinterface:1.3.3' - implementation 'androidx.fragment:fragment-ktx:1.5.2' + implementation 'androidx.fragment:fragment-ktx:1.5.3' def androidx_lifecycle_version = '2.5.1' implementation "androidx.lifecycle:lifecycle-common-java8:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidx_lifecycle_version" From cfb77f03983f3bfb80fc0b7c78cc50295d8cb238 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 30 Sep 2022 23:27:09 -0700 Subject: [PATCH 026/326] [Feature] Update Android Studio. --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index eb9c4a782..38ddc56cf 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1c31d5b3..6b97a9c04 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Sep 23 01:27:10 PDT 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 477e073c29e1774529d8609c057ce3716d177db3 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 1 Oct 2022 23:14:28 -0700 Subject: [PATCH 027/326] [Feature] Update Coil and apply MediaMetadataRetriever BitmapParams fix. --- app/build.gradle | 2 +- .../android/files/coil/VideoFrameFetcher.kt | 13 ++++++++-- .../compat/MediaMetadataRetrieverCompat.kt | 26 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d9048f8f9..3d6f6fc09 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,7 +161,7 @@ dependencies { implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' implementation 'dev.rikka.shizuku:api:12.0.0' implementation 'eu.agno3.jcifs:jcifs-ng:2.1.6' - def coil_version = '2.2.1' + def coil_version = '2.2.2' implementation "io.coil-kt:coil:$coil_version" implementation "io.coil-kt:coil-gif:$coil_version" implementation "io.coil-kt:coil-svg:$coil_version" diff --git a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt index 9f92f9b49..a02f2b67c 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt @@ -20,6 +20,8 @@ import coil.fetch.Fetcher import coil.request.Options import coil.request.videoFrameOption import coil.request.videoFramePercent +import me.zhanghai.android.files.compat.getFrameAtTimeCompat +import me.zhanghai.android.files.compat.getScaledFrameAtTimeCompat import java.util.concurrent.TimeUnit import kotlin.math.roundToInt import kotlin.math.roundToLong @@ -66,6 +68,11 @@ class VideoFrameFetcher( ) val frameOption = options.parameters.videoFrameOption() ?: MediaMetadataRetriever.OPTION_CLOSEST_SYNC + val bitmapParams = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + MediaMetadataRetriever.BitmapParams().apply { preferredConfig = options.config } + } else { + null + } val outBitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && srcWidth > 0 && srcHeight > 0) { val dstWidth = options.size.widthPx(options.scale) { srcWidth } @@ -84,9 +91,11 @@ class VideoFrameFetcher( } val width = (scale * srcWidth).roundToInt() val height = (scale * srcHeight).roundToInt() - retriever.getScaledFrameAtTime(frameMicros, frameOption, width, height) + retriever.getScaledFrameAtTimeCompat( + frameMicros, frameOption, width, height, bitmapParams + ) } else { - retriever.getFrameAtTime(frameMicros, frameOption)?.also { + retriever.getFrameAtTimeCompat(frameMicros, frameOption, bitmapParams)?.also { srcWidth = it.width srcHeight = it.height } diff --git a/app/src/main/java/me/zhanghai/android/files/compat/MediaMetadataRetrieverCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/MediaMetadataRetrieverCompat.kt index 972723fb0..012f29d53 100644 --- a/app/src/main/java/me/zhanghai/android/files/compat/MediaMetadataRetrieverCompat.kt +++ b/app/src/main/java/me/zhanghai/android/files/compat/MediaMetadataRetrieverCompat.kt @@ -5,6 +5,7 @@ package me.zhanghai.android.files.compat +import android.graphics.Bitmap import android.media.MediaMetadataRetriever import android.os.Build import androidx.annotation.RequiresApi @@ -17,6 +18,31 @@ val KClass.METADATA_KEY_SAMPLERATE: Int @RequiresApi(Build.VERSION_CODES.Q) get() = 38 +fun MediaMetadataRetriever.getFrameAtTimeCompat( + timeUs: Long, + option: Int, + params: MediaMetadataRetriever.BitmapParams? +): Bitmap? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && params != null) { + getFrameAtTime(timeUs, option, params) + } else { + getFrameAtTime(timeUs, option) + } + +@RequiresApi(Build.VERSION_CODES.O_MR1) +fun MediaMetadataRetriever.getScaledFrameAtTimeCompat( + timeUs: Long, + option: Int, + dstWidth: Int, + dstHeight: Int, + params: MediaMetadataRetriever.BitmapParams? +): Bitmap? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && params != null) { + getScaledFrameAtTime(timeUs, option, dstWidth, dstHeight, params) + } else { + getScaledFrameAtTime(timeUs, option, dstWidth, dstHeight) + } + @OptIn(ExperimentalContracts::class) inline fun MediaMetadataRetriever.use(block: (MediaMetadataRetriever) -> R): R { contract { From db1c5a1eac6f7284f62ebd817a7c8625500d6ac5 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 2 Oct 2022 12:17:55 -0700 Subject: [PATCH 028/326] [Feature] Update libsu to 5.0.3. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 3d6f6fc09..ec7087c86 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -91,7 +91,7 @@ repositories { dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' releaseImplementation 'com.github.mypplication:stetho-noop:1.1' - implementation 'com.github.topjohnwu.libsu:service:5.0.2' + implementation 'com.github.topjohnwu.libsu:service:5.0.3' } dependencies { From a22646d06fc23239d6883a4535380db583ec5a87 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 2 Oct 2022 15:59:24 -0700 Subject: [PATCH 029/326] [Fix] Make ByteStringListPath check class and file system. Fixes: #810 --- .../provider/common/ByteStringListPath.kt | 31 +++++++++++++++---- .../files/provider/common/PathExtensions.kt | 6 ++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/ByteStringListPath.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/ByteStringListPath.kt index 674269f7f..c6b9e9d1d 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/ByteStringListPath.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/ByteStringListPath.kt @@ -91,7 +91,8 @@ abstract class ByteStringListPath> : AbstractPath, if (this === other) { return true } - if (other.javaClass != javaClass) { + if (javaClass != other.javaClass || provider != other.provider + || fileSystem != other.fileSystem) { return false } other as ByteStringListPath<*> @@ -104,7 +105,8 @@ abstract class ByteStringListPath> : AbstractPath, if (this === other) { return true } - if (javaClass != other.javaClass) { + if (javaClass != other.javaClass || provider != other.provider + || fileSystem != other.fileSystem) { return false } other as ByteStringListPath<*> @@ -142,8 +144,14 @@ abstract class ByteStringListPath> : AbstractPath, } override fun resolve(other: Path): T { + if (javaClass != other.javaClass || provider != other.provider) { + throw ProviderMismatchException(other.toString()) + } @Suppress("UNCHECKED_CAST") - other as? T ?: throw ProviderMismatchException(other.toString()) + other as T + require(fileSystem == other.fileSystem) { + "The other path must have the same file system as this path" + } if (other.isAbsolute) { return other } @@ -163,9 +171,15 @@ abstract class ByteStringListPath> : AbstractPath, fun resolveSibling(other: ByteString): T = resolveSibling(createPath(other)) override fun relativize(other: Path): T { + if (javaClass != other.javaClass || provider != other.provider) { + throw ProviderMismatchException(other.toString()) + } @Suppress("UNCHECKED_CAST") - other as? T ?: throw ProviderMismatchException(other.toString()) - require(other.isAbsolute == isAbsolute) { + other as T + require(fileSystem == other.fileSystem) { + "The other path must have the same file system as this path" + } + require(isAbsolute == other.isAbsolute) { "The other path must be as absolute as this path" } if (isEmpty) { @@ -245,7 +259,12 @@ abstract class ByteStringListPath> : AbstractPath, override fun hashCode(): Int = hash(separator, segments, isAbsolute, fileSystem) override fun compareTo(other: Path): Int { - other as? ByteStringListPath<*> ?: throw ProviderMismatchException(other.toString()) + javaClass.cast(other) + @Suppress("UNCHECKED_CAST") + other as T + if (provider != other.provider) { + throw ClassCastException(other.toString()) + } return toByteString().compareTo(other.toByteString()) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/PathExtensions.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/PathExtensions.kt index fd3d4ad84..c3d56b485 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/PathExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/PathExtensions.kt @@ -321,7 +321,8 @@ fun Path.readSymbolicLinkByteString(): ByteString { fun Path.resolveForeign(other: Path): Path { asByteStringListPath() other.asByteStringListPath() - if (provider == other.provider) { + if (javaClass == other.javaClass && provider == other.provider + && fileSystem == other.fileSystem) { return resolve(other) } if (other.isAbsolute) { @@ -330,9 +331,6 @@ fun Path.resolveForeign(other: Path): Path { if (other.isEmpty) { return this } - // TODO: kotlinc: None of the following functions can be called with the arguments supplied: - // public abstract fun resolve(p0: Path!): Path! defined in java8.nio.file.Path - // public abstract fun resolve(p0: String!): Path! defined in java8.nio.file.Path var result: ByteStringListPath<*> = this for (name in other.nameByteStrings) { result = result.resolve(name) From 85276f94fe1b93eb2dfac00f05b671eaba55bfa2 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 5 Oct 2022 20:13:14 -0700 Subject: [PATCH 030/326] [Feature] Update dependency. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index ec7087c86..29d7d61a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -105,7 +105,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.exifinterface:exifinterface:1.3.3' + implementation 'androidx.exifinterface:exifinterface:1.3.4' implementation 'androidx.fragment:fragment-ktx:1.5.3' def androidx_lifecycle_version = '2.5.1' implementation "androidx.lifecycle:lifecycle-common-java8:$androidx_lifecycle_version" From 39a50e5d04c5d11a74ba001791cad776ad495c82 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 7 Oct 2022 20:51:44 -0700 Subject: [PATCH 031/326] [Feature] Update Shizuku-API. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 29d7d61a3..904515f09 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -159,7 +159,7 @@ dependencies { // dev.chrisbanesinsetter:insetter:0.6.0 makes inset unstable when entering immersive. implementation 'dev.chrisbanes:insetter-ktx:0.3.1' implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' - implementation 'dev.rikka.shizuku:api:12.0.0' + implementation 'dev.rikka.shizuku:api:12.2.0' implementation 'eu.agno3.jcifs:jcifs-ng:2.1.6' def coil_version = '2.2.2' implementation "io.coil-kt:coil:$coil_version" From bbd5040a5aa3308a62c33a575d856bb8a0fda7c7 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 8 Oct 2022 16:56:20 -0700 Subject: [PATCH 032/326] [Feature] Lay out in display cutout mode. This makes the cutout area show in app color instead of black. This is what GApps are doing and they handle list items and scroll bar in the same way as this app. --- app/src/main/res/values-v28/themes.xml | 5 +++++ app/src/main/res/values-v28/themes_material3.xml | 5 +++++ app/src/main/res/values-v29/themes.xml | 2 +- app/src/main/res/values-v29/themes_material3.xml | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-v28/themes.xml b/app/src/main/res/values-v28/themes.xml index 8005d5284..1aff8844e 100644 --- a/app/src/main/res/values-v28/themes.xml +++ b/app/src/main/res/values-v28/themes.xml @@ -7,6 +7,11 @@ + + diff --git a/app/src/main/res/values-v28/themes_material3.xml b/app/src/main/res/values-v28/themes_material3.xml index 8b19741d4..942994198 100644 --- a/app/src/main/res/values-v28/themes_material3.xml +++ b/app/src/main/res/values-v28/themes_material3.xml @@ -7,6 +7,11 @@ + + diff --git a/app/src/main/res/values-v29/themes.xml b/app/src/main/res/values-v29/themes.xml index 319dd7170..a9400a7f0 100644 --- a/app/src/main/res/values-v29/themes.xml +++ b/app/src/main/res/values-v29/themes.xml @@ -7,7 +7,7 @@ - - - + diff --git a/app/src/main/res/values/styles_material3.xml b/app/src/main/res/values/styles_material3.xml index c661d1b99..0bced815b 100644 --- a/app/src/main/res/values/styles_material3.xml +++ b/app/src/main/res/values/styles_material3.xml @@ -16,6 +16,10 @@ @drawable/m3_popupmenu_background_overlay + + + + @@ -25,10 +29,6 @@ false - - - + + + + + + @@ -81,8 +81,8 @@ @style/Widget.MaterialFiles.CardView @style/Widget.MaterialFiles.NavigationView @style/Preference.MaterialFiles.SwitchPreferenceCompat - 0dp ?textInputOutlinedStyle + 0dp @color/dark_50_percent diff --git a/app/src/main/res/values/themes_material3.xml b/app/src/main/res/values/themes_material3.xml index a7e40add7..463901aa5 100644 --- a/app/src/main/res/values/themes_material3.xml +++ b/app/src/main/res/values/themes_material3.xml @@ -21,19 +21,22 @@ true ?colorPrimary + @style/ShapeAppearance.MaterialFiles.Material3.SmallComponent + @style/ShapeAppearance.MaterialFiles.Material3.MediumComponent + @style/ShapeAppearance.MaterialFiles.Material3.LargeComponent + @style/TextAppearance.MaterialFiles.Material3.ListItem + ?textAppearanceListItem + @style/TextAppearance.MaterialFiles.Material3.ListItemSmall + ?textAppearanceListItemSmall ?floatingActionButtonSecondaryStyle @style/Widget.MaterialFiles.CardView - 0dp @style/Widget.MaterialFiles.Material3.NavigationView @style/Preference.MaterialFiles.Material3.SimpleMenuPreference @style/Preference.MaterialFiles.Material3.SwitchPreferenceCompat @style/Widget.MaterialFiles.Material3.TabLayout - @style/TextAppearance.MaterialFiles.Material3.ListItem - ?textAppearanceListItem - @style/TextAppearance.MaterialFiles.Material3.ListItemSmall - ?textAppearanceListItemSmall ?textInputOutlinedStyle @style/Widget.MaterialFiles.Material3.Toolbar + 0dp ?colorSurface @@ -72,19 +75,22 @@ true ?colorPrimary + @style/ShapeAppearance.MaterialFiles.Material3.SmallComponent + @style/ShapeAppearance.MaterialFiles.Material3.MediumComponent + @style/ShapeAppearance.MaterialFiles.Material3.LargeComponent + @style/TextAppearance.MaterialFiles.Material3.ListItem + ?textAppearanceListItem + @style/TextAppearance.MaterialFiles.Material3.ListItemSmall + ?textAppearanceListItemSmall ?floatingActionButtonSecondaryStyle @style/Widget.MaterialFiles.CardView - 0dp @style/Widget.MaterialFiles.Material3.NavigationView @style/Preference.MaterialFiles.Material3.SimpleMenuPreference @style/Preference.MaterialFiles.Material3.SwitchPreferenceCompat @style/Widget.MaterialFiles.Material3.TabLayout - @style/TextAppearance.MaterialFiles.Material3.ListItem - ?textAppearanceListItem - @style/TextAppearance.MaterialFiles.Material3.ListItemSmall - ?textAppearanceListItemSmall ?textInputOutlinedStyle @style/Widget.MaterialFiles.Material3.Toolbar + 0dp @color/dark_50_percent From efdcb8c7a55d0a6dfbb5af42e8e56c7f97566613 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 9 Aug 2023 12:00:17 -0700 Subject: [PATCH 080/326] [Fix] Fix standard directories settings not getting Material 3 switches. We should now always use PreferenceFragmentCompat.requireContext() instead of PreferenceManager.context, which is incorrectly "fixed" by Takisoft PreferenceX but we still need that for other things like dialogs etc. The difference here is whether it's Theme.applyStyle(force = false), v.s. ContextThemeWrapper(themeResId), the former correctly retains what has been explicitly set. --- .../files/settings/StandardDirectoryListPreferenceFragment.kt | 3 +-- .../me/zhanghai/android/files/ui/PreferenceFragmentCompat.kt | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/settings/StandardDirectoryListPreferenceFragment.kt b/app/src/main/java/me/zhanghai/android/files/settings/StandardDirectoryListPreferenceFragment.kt index 55de84bc0..a9f520080 100644 --- a/app/src/main/java/me/zhanghai/android/files/settings/StandardDirectoryListPreferenceFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/settings/StandardDirectoryListPreferenceFragment.kt @@ -33,9 +33,8 @@ class StandardDirectoryListPreferenceFragment : PreferenceFragmentCompat(), private fun onStandardDirectoriesChanged( standardDirectories: List ) { - val preferenceManager = preferenceManager - val context = preferenceManager.context var preferenceScreen = preferenceScreen + val context = requireContext() val oldPreferences = mutableMapOf() if (preferenceScreen == null) { preferenceScreen = preferenceManager.createPreferenceScreen(context) diff --git a/app/src/main/java/me/zhanghai/android/files/ui/PreferenceFragmentCompat.kt b/app/src/main/java/me/zhanghai/android/files/ui/PreferenceFragmentCompat.kt index 4843472fe..9f8d7fea5 100644 --- a/app/src/main/java/me/zhanghai/android/files/ui/PreferenceFragmentCompat.kt +++ b/app/src/main/java/me/zhanghai/android/files/ui/PreferenceFragmentCompat.kt @@ -15,9 +15,7 @@ abstract class PreferenceFragmentCompat : TakisoftPreferenceFragmentCompat() { // @see https://github.com/Gericop/Android-Support-Preference-V7-Fix/issues/201 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (preferenceScreen == null) { - val preferenceManager = preferenceManager - val context = preferenceManager.context - val preferenceScreen = preferenceManager.createPreferenceScreen(context) + val preferenceScreen = preferenceManager.createPreferenceScreen(requireContext()) setPreferenceScreen(preferenceScreen) } From 548d10dbe05de41f1ccd8c123132999f4c9adbfb Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 9 Aug 2023 22:25:30 -0700 Subject: [PATCH 081/326] [Fix] Use correct data source type in PathAttributesFetcher. Fixes: #730 --- .../me/zhanghai/android/files/coil/CoilUtils.kt | 15 +++++++++++++++ .../android/files/coil/PathAttributesFetcher.kt | 7 +++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt b/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt index 4c67688ae..5e1da34de 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt @@ -7,17 +7,32 @@ package me.zhanghai.android.files.coil import android.graphics.Bitmap import android.os.Build +import coil.decode.DataSource import coil.size.Dimension import coil.size.Scale import coil.size.Size import coil.size.isOriginal import coil.size.pxOrElse +import java8.nio.file.Path +import me.zhanghai.android.files.provider.archive.archiveFile +import me.zhanghai.android.files.provider.archive.isArchivePath +import me.zhanghai.android.files.provider.ftp.isFtpPath +import me.zhanghai.android.files.provider.sftp.isSftpPath +import me.zhanghai.android.files.provider.smb.isSmbPath val Bitmap.Config.isHardware: Boolean get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this == Bitmap.Config.HARDWARE fun Bitmap.Config.toSoftware(): Bitmap.Config = if (isHardware) Bitmap.Config.ARGB_8888 else this +val Path.dataSource: DataSource + get() = + when { + isArchivePath -> archiveFile.dataSource + isFtpPath || isSftpPath || isSmbPath -> DataSource.NETWORK + else -> DataSource.DISK + } + inline fun Size.widthPx(scale: Scale, original: () -> Int): Int = if (isOriginal) original() else width.toPx(scale) diff --git a/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt index a4ebbbdbd..627abe8db 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt @@ -11,7 +11,6 @@ import android.media.MediaMetadataRetriever import android.os.ParcelFileDescriptor import androidx.core.graphics.drawable.toDrawable import coil.ImageLoader -import coil.decode.DataSource import coil.decode.ImageSource import coil.fetch.DrawableResult import coil.fetch.FetchResult @@ -90,7 +89,7 @@ class PathAttributesFetcher( } if (thumbnail != null) { return DrawableResult( - thumbnail.toDrawable(options.context.resources), true, DataSource.DISK + thumbnail.toDrawable(options.context.resources), true, path.dataSource ) } } @@ -116,7 +115,7 @@ class PathAttributesFetcher( val inputStream = path.newInputStream() return SourceResult( ImageSource(inputStream.source().buffer(), options.context), - if (mimeType != MimeType.GENERIC) mimeType.value else null, DataSource.DISK + if (mimeType != MimeType.GENERIC) mimeType.value else null, path.dataSource ) } mimeType.isMedia && (path.isLinuxPath || path.isDocumentPath) -> { @@ -133,7 +132,7 @@ class PathAttributesFetcher( return SourceResult( ImageSource( embeddedPicture.inputStream().source().buffer(), options.context - ), null, DataSource.DISK + ), null, path.dataSource ) } if (mimeType.isVideo) { From f1d9b7485361814e8b90bfd5c134b60e67c07198 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 9 Aug 2023 22:58:55 -0700 Subject: [PATCH 082/326] [Fix] Always add square brackets to IPv6 hosts. So that when being converted to URI and converted back, there won't be a mismatch. IPv6 host should always contain square brackets according to javadoc of URI.getHost(). Fixes: #811 --- .../zhanghai/android/files/storage/EditFtpServerFragment.kt | 1 + .../zhanghai/android/files/storage/EditSftpServerFragment.kt | 1 + .../zhanghai/android/files/storage/EditSmbServerFragment.kt | 1 + .../java/me/zhanghai/android/files/storage/URIExtensions.kt | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt index b8982c5a3..bde9348c0 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt @@ -311,6 +311,7 @@ class EditFtpServerFragment : Fragment() { private fun getServerOrSetError(): FtpServer? { var errorEdit: TextInputEditText? = null val host = binding.hostEdit.text.toString().takeIfNotEmpty() + ?.let { URI::class.canonicalizeHost(it) } if (host == null) { binding.hostLayout.error = getString(R.string.storage_edit_ftp_server_host_error_empty) if (errorEdit == null) { diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditSftpServerFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditSftpServerFragment.kt index 079fc2a18..42ec61b4e 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditSftpServerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditSftpServerFragment.kt @@ -280,6 +280,7 @@ class EditSftpServerFragment : Fragment() { private fun getServerOrSetError(): SftpServer? { var errorEdit: TextInputEditText? = null val host = binding.hostEdit.text.toString().takeIfNotEmpty() + ?.let { URI::class.canonicalizeHost(it) } if (host == null) { binding.hostLayout.error = getString(R.string.storage_edit_sftp_server_host_error_empty) diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditSmbServerFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditSmbServerFragment.kt index bd81ed376..eeea3fa20 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditSmbServerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditSmbServerFragment.kt @@ -258,6 +258,7 @@ class EditSmbServerFragment : Fragment() { private fun getServerOrSetError(): SmbServer? { var errorEdit: TextInputEditText? = null val host = binding.hostEdit.text.toString().takeIfNotEmpty() + ?.let { URI::class.canonicalizeHost(it) } if (host == null) { binding.hostLayout.error = getString(R.string.storage_edit_smb_server_host_error_empty) if (errorEdit == null) { diff --git a/app/src/main/java/me/zhanghai/android/files/storage/URIExtensions.kt b/app/src/main/java/me/zhanghai/android/files/storage/URIExtensions.kt index 3d2147ac0..706f6d6be 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/URIExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/URIExtensions.kt @@ -4,6 +4,10 @@ import java.net.URI import java.net.URISyntaxException import kotlin.reflect.KClass +// @see URI.appendAuthority +fun KClass.canonicalizeHost(host: String): String = + if (host.contains(':') && !host.startsWith('[') && !host.endsWith(']')) "[$host]" else host + fun KClass.createOrLog(uri: String): URI? = try { URI(uri) From 3dca5a7c652066f26e0d131c8dbca1c5dcf0d853 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 10 Aug 2023 02:28:23 -0700 Subject: [PATCH 083/326] [Feature] Add TV support. Fixes: #987 --- app/src/main/AndroidManifest.xml | 4 + app/src/main/res/drawable-xhdpi/banner.png | Bin 0 -> 6432 bytes app/src/main/res/values/themes.xml | 6 + app/src/main/res/values/themes_material3.xml | 6 + art/banner-play.png | Bin 0 -> 29212 bytes art/banner-xhdpi.png | Bin 0 -> 6432 bytes art/banner.svg | 286 ++++++++++++++++++ art/deploy-png.sh | 3 + art/generate-png.sh | 3 + .../android/en-US/images/tvBanner.png | Bin 0 -> 29212 bytes 10 files changed, 308 insertions(+) create mode 100644 app/src/main/res/drawable-xhdpi/banner.png create mode 100644 art/banner-play.png create mode 100644 art/banner-xhdpi.png create mode 100644 art/banner.svg create mode 100644 fastlane/metadata/android/en-US/images/tvBanner.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c37ee903..fb715fcef 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + + @@ -38,6 +40,7 @@ --> + diff --git a/app/src/main/res/drawable-xhdpi/banner.png b/app/src/main/res/drawable-xhdpi/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..69ef794d4d4d8ee1c8ad7fe20ea80ee83ef4d77b GIT binary patch literal 6432 zcmeHM)mxNbxSgRxnn6HXX(XgWsR5)VhHfN=8oHEj5Red%4oLy&?i59k?(P_d4jG0s zzuR+j{(*Dx?QcKp?tP!N*Spr{Sr005Lh?;G%N(Cg0j6&d=6 z@A}rz0{|fI|L+41!^+;HK^jj515X{8t*4KryA8m{$A=f{;^<**>1x9ZbGOSnk)j0v zsH>F0vbw(6@D)E#-KzrJQ-rDQxSX8aXUH|jPnJzZS-Cvz@3Z>p74t>64gYxl9hN)8 zp|7v6=Jg$~AeBGH+hC2njYa`)SGa&6EK#rw1_345Fhz~+>j=*cYL!uRWHg6WEqP@0 zMK@wEJ3B?$w$)>A>;`rH{Zoru9)K(wsgt*UZU*-7gPl>=I^OcdUU} zeO!RX=a1B(;OWjj&rm!HQnobH>6=r`;4MY*)4w~#99~~Z`)YE#=bU4~FXE(d^8w+( z{OY4L-`B&7?im7eDoJ@{%csy#k5KR!|1q2d+($x2B;r?z;p6R|5zNo;{7rE0hwCy2 zJA;{aoReMJukUBkWD}W;Kan;hS#NL`C*HeMrB($m)-arIk~T&HP&wwnVhNt zFm((wTGn9Hq%9PYd)!;|3Uoz8R{0<`3HxzX_0?th-;ZL`<~)<9eVV9AG?tz)ZqvC& zkQb{j8gVvpV*e|kOLQ&HQRBjUrR(8~9ugJb^wGt3V#I?zdcRDII9sIcRbr^BhWp01 zGYKl`zZgO709~TlYdgkJkQr!7vE=0B;U!xWJn_mtOJ#+?HbKMrjlGj#?cML(*K-zL z_t?cPr&uHa3uq3B-9r}{BSM)d!pdz^4A;c}dU)zY5%W=sj?V@)zUiJVFoO+f@eU#J z@}cWX2piNVcq!sXAY*xNiszBPZJvHMLmPPNB^ub;K_AOcp%S|ARd^_by_z=p{3hNU z?6zAs#PE7Iuq_UWlN_4HI7*>tzedVUimN!m%IjTM%4+2d;eH<8i@I z67M;`Xj7NuTchLeplzw5iRx!6^Ig@!ut<-bpqj-> zg71M|bnio;M>)TsEEW^FJReoq1lg-WMF_+`6UTI}pn^+x5D@P0AhZ4c?-f+X#P=lZ zy7ENQ@e}4wZePx-U1EH9pyp!bIea(>fBgaz$Ke+Qxiy7?V;zuaRuWlGt&T#jHLn+! z{L|kY49}GM2~3w-4YFH0SsJ_Q5eR|C9B@B@S)vE6PW_}#eV5-&Z*n&e7iu+N%iF_X zQBwz4qtr~Ec+uHcn0y(V;S~T|s=IEB*rT65hf0Ezo?VpBgMJRce>70}3;H`>HSz0x zBp>^SiZW71{@V$;4VU+<+MfVCldBUhir2f0>Omr=q7`MZW&uD3J9fofZRyi>d zQ*>F$E;jL0T~WWtr&x3FQj6H>XJJwoZ@~jBZ*Ac89GN2|1j>*ojHV)GIV|cM#iqiD zrIGiV`CCZb4Q74A$g+WFp$MF~5ov)ev9w&i(42!$m53!$2Y-13nxas#fPJJL^3lBZ zyokPx{8FAFrlu63FFOtBau7lDcb`l(KPB?uwAJ7I_~ zKMWOCkP+?xE^m5~)4LFAs{rb`BM;6p`f6+Y7=kDqp(gx~9qh3d-)=K)&&wxEtv)tJ zYBz`O=J%9z3V`E}Z(`(i&?-SMT+F2gq{B1lZ)B6%Y9nZHTON~z#cc&eUy~<bwkZMYa(t)iW!why``muIf8 zdUoXbeXNi{`fm<3mx%YhIa=%!aQDW&t!Jr~RA3HbY^Qr(%*#!MAm;$QBh>YAxBmRl zS2I_bPTmqQk-lKdZjEOUIig{Y4~my*@3D3aZ8^FUX;2bGP~@rhCXR9RZl=8EP?m6u zw8r3WRy;jjlI^%K+*q_D)WsIjf=KW$CX*kuC6K3M1XUtBdu&{=xtk*eP8{`uM`^cY zzADM8a$H=@vNk-zp+ct+5AVSo=yNaR+SM0V?&1|E0*j+d8vSC+ptu;N zv?t0mPjbZ`M)$Ox5>-Wcz>wg%K!X_knXlbAyr#V%k4`i%wl?pxNVz9#2g?!7u$R+z z>OLE2ErLq6p>?y`c)!1#BD-Z9Rnky=TfoYd+unqc;y8&gPOn~nHf3@z zZXW7gz8Nl1ox6)T(Mil**ZM}hiqj|utzDv=9=K73fD4Mn$HQkVuHto)KbueD3dHTD z-&(Ux>cL$krsg1AaG`;vc0QeQ<6e2y$Zz51=SQ=t<-YbKr2B@)OKuQ}v{^p^k58|W z81TZS6eWogz$`-TVH0~%CvPtmU&AC&<&5}#qdBF!|LHgF_w>CBqG1&*8s-T`Wk z3+p~@q}$)4sNbLpT`{K#^>AMfG4m~q{PAw5pEYQFtLFJ-^w1#(J-q&H@arh;H!+pN z@cNT>#9!C~G=Gq6EK2a@KGx9xCs12YwWv#GBCprc467XVe-%}-KAH0RcIMnSaq~F> z>uhnCze{+EtIEshOqu#S?IvVZGnsfB@#ki9Kb(K1Ml$v&@7#J=7PE+37>P=D&lVda zs2dTM;LU*dP95odUyn&(JnA_Z9%?SA{R0+b`#U|`KE9*rDBrRTCYeQi+6%Om}>!)pPh0|Q?*w}3)6I4_gxX1@urjBbE*XV#mV>^7@s9Q5{2TQ-wy(JC`@ z{W>0}>&8HE6$J&Utjwl6Lvs+(KwnuUNC)eSQ%23Zvwu|F!LJR|B2%2_DF#JP90u5x ztxt%t)Ru462xr$!W@dQBx{Uhg3o@OFPb_pKO@{lzq2XEAL$3WgsOqueL~kXxhm=66 z?gsK;h+r#vo=f}wTD!^^u*L!~m{JU2!Ui?zo_Qbu0J=O=K@y_1xB5QoY-BlG=X`rnE&(;m`M@4kN}56m%Jc#mkwFsQPF(8I@+w~jHJ z@&cr%L4h$#mkx5JBexEJ^sJ7@yn+&<7y;pJ&`;9I+owmDU)FktzBQGr<&9go4GDb^ z91ZxssO>*H2miEr5q3tt)@M}eJkz>ep7U(7QVr9NtC8b z1(`vsFxquC;0Q^2VV7!nZpMdSR}G9M%@8G-=?9?O(gnQ=n zQWyk981FVA@&;1&LxN+X)`&L`!)Hnpx#mun8$rGwok_cmR*i(}9jSvA*5Ponfzexw zl{U^ZK(=nt64kF`=c}7@oz50!f*83md0r`L ze!eri1FXE`@lY1ciV*bYx|`>?;4lnw83|i|bW{L5k1P}Wpx6FLKx0s5ytSR zzaDyr6VsRVa_$AMxh%Fh9!nD4*IN<#-#TDAfOcXUd!nqe%ktMo<>LC@j3PAxaAAl5(B6E(8+BD`>8xh5zmP0M+)#c2*u}U>``GsA~%b;K-eZziPZ~9kG zzs;SODd27gcANb4qB&5K?thVZf%huhB8YfzA~%D8slP6z;aB07MzS+^5d;d@F1`}& zaaiJ?Ck3oKEtkEvl}RqV{KaLk==I^a&E}($meW+} za2>XRD7nhx!P9i^U4JKvg*mjO*F|Nz>&5;WV2s7h*U>pWAG!`rxYfLFRMs#~^e{G9 z@B5IYjU#^3C}_`g#;(r#L=h`3X!(ZW23;5x@|r*J-yd=l0nt_7{b|s$pC!%iN?_Sq z;;`C0p~;J6RUg6p8{y}B2nk-F8yu}+xB^@0$H+#C#sBdntwGCVvt{G)d6JmXBQBkK==yS-t@7I zjL&~HRzqW3&)1E6Q?WS`UaqsWpMPw_l(8cDF;{Nk))!h1bB?>L$Xr)GT)4p1$$PIG z5xL4UHf0(G$9-&I++O+P!5hB)Fqv(sahM_G$WCmFVE^b6Yh`MN&bYO|S5tM8w?4!1 zaV*G`8Me}?RXU-Mn89h^_`KUxU2_ASym9@+pxA2!=DYoNE5!HLW zSs?6q0G>7Krks5Z+P#5B!dEw;n$OBhdD8BS6j*O33X>ZIKMmW9XgEix8?Jjk~c}h+xhd2yDP>?%A7@waXlBS6McBi%2_cDjcQ@wZQqVx8e$4Bqph9637 zR4+y@kJznO<+sbm40>P+?^I{|B}L$nI9Y37LC7BOu~{oDixl3dtL9bjy&~*HC+eq; zZxSu~PpC6P+0cb@3Wx0G$K3cRH!~f|ys5fIG|h>EU03ApX`1=s7^S*I!oTm`w^gv3 zm4>$S@iwp6{Z1EHHeQSTK$TwV7unL7XSlUR&iBc4^~@#Pr^8DzNaVnug%9R5toqF4 zc6RiZCTQ0l*Vat;_BV4!Gk0v_4cIyTuEQaBt|`*;1)J+a{~zCjwqItANyNI34-U`J zNv5f|n@Ziwa$-8S5x0$=fNHkQ>4=c6eeci75AMk|f{WS;0%So*YuR?!E=PXZ{twfq z=0|+hT_nHF|FiW+ppZ|GYfDb+ne3T55-%t56$Z)pmQ#<>`Nqi+<+bO^n6`EppC>h! zd{*HpfZ1}kK;<+~|I!{*MJlHiqRMEmVwm0@l3_?3Sj;>Bc_xLStsCsVwC)K z{$uxg*b|hLw`VNobBj(J89f(_OUFLiB z!nQ#TlCj+|6{0p*we??0S#D!bM0urW!WOLQo1pHf&oMqe;4@?t+#qS!9S|7|e{E}Q zG~XcVa8#_(#TM(8N6p*OjQ{B8g0CU9Xf&;Z=h)dRX$Bo^OL#zSD#69(1zb+!N~S-f z>K0Jq-?Bh%FOGZ^YXu8T>q;F3T;}N;bHp&=<%w%<9JWhiuU9IaI>%v~qc-sdujazy zG)^tfU00fA97paB>kK|47%cq>VF$~(DhZ5Qvr@t#x+vka+#XulpH+!2yISJ3G zLSJnT1^96Ud3S{KGpk`nV_P90)kz}S+AF}uqRsIrBNxb^G?Us)Tj9+PpoacSfc!YmtFTr zTNRk7p*s6aF&E|F;gh2g?Q3~e-PBeP*s7;jub!hCcbQWYh-dH=EXu4#!QW4sMA}gR z_dQ7aVR?9ah;)+U8??eX@wLTF>tqNv(#BM{k<;?}E+pB5@i}_)oGy4)VZxG@wAPzx zG4kz}_h{B}f2+rEL50*!81Fp`tzc~cjF7>k78F>FV|0MWK`fdtQBY>2QLbRlm;z@G!U`pUGx%kT!IdcTBvS@S zSxN)uG8rvOJV-ODkmGO1OmKT27qfRNC835^z6?-^{+-bGU*9Dv*9@;%X(`W_2`fF{ zM><609oEPxPB7`>0)WMMbtE#w-bR*03iW}yGX&B&j1&3qfydSwRC_EoTw>KhRSs8~^|S literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 652ca5a5d..a267a9d19 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -18,6 +18,9 @@ ?colorSurface @style/Widget.MaterialFiles.ListView.DropDown false + + true + true true @color/color_primary @@ -67,6 +70,9 @@ ?colorSurface @style/Widget.MaterialFiles.ListView.DropDown false + + true + true @android:color/black true diff --git a/app/src/main/res/values/themes_material3.xml b/app/src/main/res/values/themes_material3.xml index 463901aa5..c3985d164 100644 --- a/app/src/main/res/values/themes_material3.xml +++ b/app/src/main/res/values/themes_material3.xml @@ -18,6 +18,9 @@ ?colorSurface @style/Widget.MaterialFiles.ListView.DropDown false + + true + true true ?colorPrimary @@ -71,6 +74,9 @@ ?colorSurface @style/Widget.MaterialFiles.ListView.DropDown false + + true + true @android:color/black true diff --git a/art/banner-play.png b/art/banner-play.png new file mode 100644 index 0000000000000000000000000000000000000000..962db1a1408461def2e74426686e7162bfbcde69 GIT binary patch literal 29212 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 literal 0 HcmV?d00001 diff --git a/art/banner.svg b/art/banner.svg new file mode 100644 index 000000000..aa77216cd --- /dev/null +++ b/art/banner.svg @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + Material Files + + diff --git a/art/deploy-png.sh b/art/deploy-png.sh index 16536be70..03ff53f86 100755 --- a/art/deploy-png.sh +++ b/art/deploy-png.sh @@ -13,3 +13,6 @@ for shortcut in directory downloads file ftp_server; do cp "${shortcut}_shortcut_icon-${dpi}.png" "../app/src/main/res/mipmap-${dpi}/${shortcut}_shortcut_icon.png" done done + +cp banner-xhdpi.png ../app/src/main/res/drawable-xhdpi/banner.png +cp banner-play.png ../fastlane/metadata/android/en-US/images/tvBanner.png diff --git a/art/generate-png.sh b/art/generate-png.sh index 079ffedca..c55070b74 100755 --- a/art/generate-png.sh +++ b/art/generate-png.sh @@ -29,3 +29,6 @@ for shortcut in directory downloads file ftp_server; do inkscape -o "${shortcut}_shortcut_icon-xxhdpi.png" --export-area=15:15:93:93 -w 144 -h 144 "${shortcut}_shortcut_icon.svg" inkscape -o "${shortcut}_shortcut_icon-xxxhdpi.png" --export-area=15:15:93:93 -w 192 -h 192 "${shortcut}_shortcut_icon.svg" done + +inkscape -o banner-xhdpi.png --export-area=0:0:320:180 -w 320 -h 180 banner.svg +inkscape -o banner-play.png --export-area=0:0:320:180 -w 1280 -h 720 banner.svg diff --git a/fastlane/metadata/android/en-US/images/tvBanner.png b/fastlane/metadata/android/en-US/images/tvBanner.png new file mode 100644 index 0000000000000000000000000000000000000000..962db1a1408461def2e74426686e7162bfbcde69 GIT binary patch literal 29212 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 Date: Thu, 10 Aug 2023 02:51:22 -0700 Subject: [PATCH 084/326] [Feature] Shorten selection title format. Otherwise the user very likely can't see the number of files in some locales. --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5dc5a7250..3037f94d4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -263,7 +263,7 @@ 添加书签 创建快捷方式 在新窗口中打开 - 已选择 %1$,d + %1$,d 选择“%1$s” 移动 %1$,d 复制 %1$,d diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index a1df2396a..8794af750 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -263,7 +263,7 @@ 新增書籤 建立捷徑 在新窗口中開啟 - 已選取 %1$,d + %1$,d 選取「%1$s」 移動 %1$,d 複製 %1$,d diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f68a0009..0887248a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -296,7 +296,7 @@ Create shortcut @string/file_list_action_copy_path Open in new window - %1$,d selected + %1$,d @string/file_item_action_extract @string/file_item_action_archive Select “%1$s” From 56bb2e7bb76ec58d07f1ccebe047ce80b97cac25 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 10 Aug 2023 03:35:22 -0700 Subject: [PATCH 085/326] [Feature] Make fast scroll popup respect sort by. Fixes: #981 --- app/build.gradle | 2 +- .../android/files/filelist/FileListAdapter.kt | 24 ++++++++++++------- .../files/filelist/FileListFragment.kt | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d5d4f2ba5..54ca88d13 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -180,7 +180,7 @@ dependencies { implementation "io.coil-kt:coil-svg" implementation "io.coil-kt:coil-video" implementation 'me.zhanghai.android.appiconloader:appiconloader:1.5.0' - implementation 'me.zhanghai.android.fastscroll:library:1.2.0' + implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.foregroundcompat:library:1.0.2' implementation 'me.zhanghai.android.libselinux:library:2.1.0' implementation 'me.zhanghai.android.retrofile:library:1.1.1' diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt index faa61a8b8..033f32248 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt @@ -56,13 +56,14 @@ class FileListAdapter( } } - private lateinit var _comparator: Comparator - var comparator: Comparator - get() = _comparator + private lateinit var _sortOptions: FileSortOptions + var sortOptions: FileSortOptions + get() = _sortOptions set(value) { - _comparator = value + _sortOptions = value if (!isSearching) { - super.replace(list.sortedWith(value), true) + val sortedList = list.sortedWith(value.createComparator()) + super.replace(sortedList, true) rebuildFilePositionMap() } } @@ -153,7 +154,8 @@ class FileListAdapter( fun replaceListAndIsSearching(list: List, isSearching: Boolean) { val clear = this.isSearching != isSearching this.isSearching = isSearching - super.replace(if (!isSearching) list.sortedWith(comparator) else list, clear) + val sortedList = if (!isSearching) list.sortedWith(sortOptions.createComparator()) else list + super.replace(sortedList, clear) rebuildFilePositionMap() } @@ -384,9 +386,15 @@ class FileListAdapter( } } - override fun getPopupText(position: Int): CharSequence { + override fun getPopupText(view: View, position: Int): CharSequence { val file = getItem(position) - return file.name.take(1).uppercase(Locale.getDefault()) + return when (sortOptions.by) { + FileSortOptions.By.NAME -> file.name.take(1).uppercase(Locale.getDefault()) + FileSortOptions.By.TYPE -> file.extension.uppercase(Locale.getDefault()) + FileSortOptions.By.SIZE -> file.attributes.fileSize.formatHumanReadable(view.context) + FileSortOptions.By.LAST_MODIFIED -> + file.attributes.lastModifiedTime().toInstant().formatShort(view.context) + } } override val isAnimationEnabled: Boolean diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index f95bf5c78..e399ee584 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -595,7 +595,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. } private fun onSortOptionsChanged(sortOptions: FileSortOptions) { - adapter.comparator = sortOptions.createComparator() + adapter.sortOptions = sortOptions updateViewSortMenuItems() } From 0efbf8303a39fd06de8a48f8d26a03a776d5e744 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 10 Aug 2023 04:08:53 -0700 Subject: [PATCH 086/326] [Refactor] Unify concept of local and remote path. --- .../me/zhanghai/android/files/coil/CoilUtils.kt | 13 ++----------- .../android/files/coil/PathAttributesFetcher.kt | 15 ++++++++------- .../android/files/filelist/FileItemExtensions.kt | 13 ++++++------- .../android/files/filelist/PathExtensions.kt | 9 +++++++++ 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt b/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt index 5e1da34de..4617f40e3 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt @@ -14,11 +14,7 @@ import coil.size.Size import coil.size.isOriginal import coil.size.pxOrElse import java8.nio.file.Path -import me.zhanghai.android.files.provider.archive.archiveFile -import me.zhanghai.android.files.provider.archive.isArchivePath -import me.zhanghai.android.files.provider.ftp.isFtpPath -import me.zhanghai.android.files.provider.sftp.isSftpPath -import me.zhanghai.android.files.provider.smb.isSmbPath +import me.zhanghai.android.files.filelist.isRemotePath val Bitmap.Config.isHardware: Boolean get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && this == Bitmap.Config.HARDWARE @@ -26,12 +22,7 @@ val Bitmap.Config.isHardware: Boolean fun Bitmap.Config.toSoftware(): Bitmap.Config = if (isHardware) Bitmap.Config.ARGB_8888 else this val Path.dataSource: DataSource - get() = - when { - isArchivePath -> archiveFile.dataSource - isFtpPath || isSftpPath || isSmbPath -> DataSource.NETWORK - else -> DataSource.DISK - } + get() = if (isRemotePath) DataSource.NETWORK else DataSource.DISK inline fun Size.widthPx(scale: Scale, original: () -> Int): Int = if (isOriginal) original() else width.toPx(scale) diff --git a/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt index 627abe8db..65f9c6a13 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt @@ -31,6 +31,7 @@ import me.zhanghai.android.files.file.isMedia import me.zhanghai.android.files.file.isPdf import me.zhanghai.android.files.file.isVideo import me.zhanghai.android.files.file.lastModifiedInstant +import me.zhanghai.android.files.filelist.isRemotePath import me.zhanghai.android.files.provider.common.AndroidFileTypeDetector import me.zhanghai.android.files.provider.common.newInputStream import me.zhanghai.android.files.provider.content.resolver.ResolverException @@ -93,13 +94,13 @@ class PathAttributesFetcher( ) } } - val isLocalPath = path.isLinuxPath - || (path.isDocumentPath && DocumentResolver.isLocal(path as DocumentResolver.Path)) - // FTP doesn't support random access and requires one connection per parallel read. - val shouldReadRemotePath = !path.isFtpPath - && Settings.READ_REMOTE_FILES_FOR_THUMBNAIL.valueCompat - if (!(isLocalPath || shouldReadRemotePath)) { - error("Cannot read $path for thumbnail") + if (path.isRemotePath) { + // FTP doesn't support random access and requires one connection per parallel read. + val shouldReadRemotePath = !path.isFtpPath + && Settings.READ_REMOTE_FILES_FOR_THUMBNAIL.valueCompat + if (!shouldReadRemotePath) { + error("Cannot read $path for thumbnail") + } } } val mimeType = AndroidFileTypeDetector.getMimeType(data.first, data.second).asMimeType() diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt index 76b94dfba..374ee7382 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt @@ -21,7 +21,6 @@ import me.zhanghai.android.files.file.isPdf import me.zhanghai.android.files.provider.archive.createArchiveRootPath import me.zhanghai.android.files.provider.document.documentSupportsThumbnail import me.zhanghai.android.files.provider.document.isDocumentPath -import me.zhanghai.android.files.provider.document.resolver.DocumentResolver import me.zhanghai.android.files.provider.ftp.isFtpPath import me.zhanghai.android.files.provider.linux.isLinuxPath import me.zhanghai.android.files.settings.Settings @@ -61,12 +60,12 @@ val FileItem.supportsThumbnail: Boolean if (path.isDocumentPath && attributes.documentSupportsThumbnail) { return true } - val isLocalPath = path.isLinuxPath - || (path.isDocumentPath && DocumentResolver.isLocal(path as DocumentResolver.Path)) - val shouldReadRemotePath = !path.isFtpPath - && Settings.READ_REMOTE_FILES_FOR_THUMBNAIL.valueCompat - if (!(isLocalPath || shouldReadRemotePath)) { - return false + if (path.isRemotePath) { + val shouldReadRemotePath = !path.isFtpPath + && Settings.READ_REMOTE_FILES_FOR_THUMBNAIL.valueCompat + if (!shouldReadRemotePath) { + return false + } } return when { mimeType.isApk && path.isGetPackageArchiveInfoCompatible -> true diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/PathExtensions.kt b/app/src/main/java/me/zhanghai/android/files/filelist/PathExtensions.kt index f249f8536..44b03acad 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/PathExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/PathExtensions.kt @@ -10,6 +10,8 @@ import me.zhanghai.android.files.file.MimeType import me.zhanghai.android.files.file.isSupportedArchive import me.zhanghai.android.files.provider.archive.archiveFile import me.zhanghai.android.files.provider.archive.isArchivePath +import me.zhanghai.android.files.provider.document.isDocumentPath +import me.zhanghai.android.files.provider.document.resolver.DocumentResolver import me.zhanghai.android.files.provider.linux.isLinuxPath val Path.name: String @@ -18,3 +20,10 @@ val Path.name: String fun Path.toUserFriendlyString(): String = if (isLinuxPath) toFile().path else toUri().toString() fun Path.isArchiveFile(mimeType: MimeType): Boolean = !isArchivePath && mimeType.isSupportedArchive + +val Path.isLocalPath: Boolean + get() = + isLinuxPath || (isDocumentPath && DocumentResolver.isLocal(this as DocumentResolver.Path)) + +val Path.isRemotePath: Boolean + get() = !isLocalPath From 5bbd452b7b1286f10b7be998ad325e5ef6bb40f8 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 19 Aug 2023 21:00:21 -0700 Subject: [PATCH 087/326] [Feature] Add document manager shortcut and use it for Android/{data,obb}. Fixes: #845 --- app/src/main/AndroidManifest.xml | 10 ++ .../files/compat/DocumentsContractCompat.kt | 12 ++ .../android/files/file/DocumentUri.kt | 8 +- .../files/navigation/NavigationFragment.kt | 41 +------ .../files/navigation/NavigationItem.kt | 7 +- .../files/navigation/NavigationItems.kt | 61 +++++++--- .../AddDocumentManagerShortcutActivity.kt | 30 +++++ .../AddDocumentManagerShortcutFragment.kt | 46 ++++++++ .../files/storage/AddDocumentTreeActivity.kt | 9 +- .../files/storage/AddDocumentTreeFragment.kt | 16 +-- .../files/storage/AddStorageDialogFragment.kt | 39 +++---- .../files/storage/DocumentManagerShortcut.kt | 45 ++++++++ .../android/files/storage/DocumentTree.kt | 3 +- ...itDocumentManagerShortcutDialogActivity.kt | 30 +++++ ...itDocumentManagerShortcutDialogFragment.kt | 109 ++++++++++++++++++ .../storage/EditDocumentTreeDialogFragment.kt | 3 +- .../zhanghai/android/files/storage/Storage.kt | 4 +- .../android/files/util/IntentExtensions.kt | 5 + .../edit_document_manager_shortcut_dialog.xml | 62 ++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 4 + app/src/main/res/values-zh-rTW/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 22 files changed, 443 insertions(+), 109 deletions(-) create mode 100644 app/src/main/java/me/zhanghai/android/files/storage/AddDocumentManagerShortcutActivity.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/storage/AddDocumentManagerShortcutFragment.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/storage/DocumentManagerShortcut.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogActivity.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt create mode 100644 app/src/main/res/layout/edit_document_manager_shortcut_dialog.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fb715fcef..664e1b797 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -173,6 +173,16 @@ android:label="@string/storage_edit_device_storage_title" android:theme="@style/Theme.MaterialFiles.Translucent" /> + + + + @@ -60,7 +63,10 @@ val navigationItems: List private val storageItems: List @Size(min = 0) - get() = Settings.STORAGES.valueCompat.filter { it.isVisible }.map { StorageItem(it) } + get() = + Settings.STORAGES.valueCompat.filter { it.isVisible }.map { + if (it.path != null) PathStorageItem(it) else IntentStorageItem(it) + } private abstract class PathItem(val path: Path) : NavigationItem() { override fun isChecked(listener: Listener): Boolean = listener.currentPath == path @@ -75,9 +81,9 @@ private abstract class PathItem(val path: Path) : NavigationItem() { } } -private class StorageItem( +private class PathStorageItem( private val storage: Storage -) : PathItem(storage.path), NavigationRoot { +) : PathItem(storage.path!!), NavigationRoot { init { require(storage.isVisible) } @@ -95,13 +101,40 @@ private class StorageItem( storage.linuxPath?.let { getStorageSubtitle(it, context) } override fun onLongClick(listener: Listener): Boolean { - listener.onEditStorage(storage) + listener.launchIntent(storage.createEditIntent()) return true } override fun getName(context: Context): String = getTitle(context) } +private class IntentStorageItem( + private val storage: Storage +) : NavigationItem() { + init { + require(storage.isVisible) + } + + override val id: Long + get() = storage.id + + override val iconRes: Int + @DrawableRes + get() = storage.iconRes + + override fun getTitle(context: Context): String = storage.getName(context) + + override fun onClick(listener: Listener) { + listener.launchIntent(storage.createIntent()!!) + listener.closeNavigationDrawer() + } + + override fun onLongClick(listener: Listener): Boolean { + listener.launchIntent(storage.createEditIntent()) + return true + } +} + private val storageVolumeItems: List @Size(min = 0) get() = @@ -162,7 +195,7 @@ private class AddStorageItem : NavigationItem() { context.getString(R.string.navigation_add_storage) override fun onClick(listener: Listener) { - listener.onAddStorage() + listener.launchIntent(AddStorageDialogActivity::class.createIntent()) } } @@ -190,7 +223,7 @@ private class StandardDirectoryItem( override fun getTitle(context: Context): String = standardDirectory.getTitle(context) override fun onLongClick(listener: Listener): Boolean { - listener.onEditStandardDirectory(standardDirectory) + listener.launchIntent(StandardDirectoryListActivity::class.createIntent()) return true } } @@ -311,7 +344,10 @@ private class BookmarkDirectoryItem( override fun getTitle(context: Context): String = bookmarkDirectory.name override fun onLongClick(listener: Listener): Boolean { - listener.onEditBookmarkDirectory(bookmarkDirectory) + listener.launchIntent( + EditBookmarkDirectoryDialogActivity::class.createIntent() + .putArgs(EditBookmarkDirectoryDialogFragment.Args(bookmarkDirectory)) + ) return true } } @@ -319,15 +355,15 @@ private class BookmarkDirectoryItem( private val menuItems: List @Size(3) get() = listOf( - ActivityMenuItem( + IntentMenuItem( R.drawable.shared_directory_icon_white_24dp, R.string.navigation_ftp_server, FtpServerActivity::class.createIntent() ), - ActivityMenuItem( + IntentMenuItem( R.drawable.settings_icon_white_24dp, R.string.navigation_settings, SettingsActivity::class.createIntent() ), - ActivityMenuItem( + IntentMenuItem( R.drawable.about_icon_white_24dp, R.string.navigation_about, AboutActivity::class.createIntent() ) @@ -340,7 +376,7 @@ private abstract class MenuItem( override fun getTitle(context: Context): String = context.getString(titleRes) } -private class ActivityMenuItem( +private class IntentMenuItem( @DrawableRes iconRes: Int, @StringRes titleRes: Int, private val intent: Intent @@ -349,8 +385,7 @@ private class ActivityMenuItem( get() = intent.component.hashCode().toLong() override fun onClick(listener: Listener) { - // TODO: startActivitySafe()? - listener.startActivity(intent) + listener.launchIntent(intent) listener.closeNavigationDrawer() } } diff --git a/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentManagerShortcutActivity.kt b/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentManagerShortcutActivity.kt new file mode 100644 index 000000000..8cb975f84 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentManagerShortcutActivity.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.storage + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.commit +import me.zhanghai.android.files.app.AppActivity +import me.zhanghai.android.files.util.args +import me.zhanghai.android.files.util.putArgs + +class AddDocumentManagerShortcutActivity : AppActivity() { + private val args by args() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Calls ensureSubDecor(). + findViewById(android.R.id.content) + if (savedInstanceState == null) { + val fragment = AddDocumentManagerShortcutFragment().putArgs(args) + supportFragmentManager.commit { + add(fragment, AddDocumentManagerShortcutFragment::class.java.name) + } + } + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentManagerShortcutFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentManagerShortcutFragment.kt new file mode 100644 index 000000000..c598bccc4 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentManagerShortcutFragment.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.storage + +import android.os.Bundle +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import kotlinx.parcelize.Parcelize +import me.zhanghai.android.files.R +import me.zhanghai.android.files.app.packageManager +import me.zhanghai.android.files.file.DocumentUri +import me.zhanghai.android.files.util.ParcelableArgs +import me.zhanghai.android.files.util.args +import me.zhanghai.android.files.util.createDocumentManagerViewDirectoryIntent +import me.zhanghai.android.files.util.finish +import me.zhanghai.android.files.util.showToast + +class AddDocumentManagerShortcutFragment : Fragment() { + private val args by args() + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + val uri = args.uri + val hasDocumentManager = uri.value.createDocumentManagerViewDirectoryIntent() + .resolveActivity(packageManager) != null + if (hasDocumentManager) { + val documentManagerShortcut = DocumentManagerShortcut( + null, args.customNameRes?.let { getString(it) }, uri + ) + Storages.addOrReplace(documentManagerShortcut) + } else { + showToast(R.string.activity_not_found) + } + finish() + } + + @Parcelize + class Args( + @StringRes val customNameRes: Int?, + val uri: DocumentUri + ) : ParcelableArgs +} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentTreeActivity.kt b/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentTreeActivity.kt index 9d5b378f7..45c4074f7 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentTreeActivity.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentTreeActivity.kt @@ -9,22 +9,15 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.commit import me.zhanghai.android.files.app.AppActivity -import me.zhanghai.android.files.util.args -import me.zhanghai.android.files.util.putArgs class AddDocumentTreeActivity : AppActivity() { - private val args by args() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Calls ensureSubDecor(). findViewById(android.R.id.content) if (savedInstanceState == null) { - val fragment = AddDocumentTreeFragment().putArgs(args) - supportFragmentManager.commit { - add(fragment, AddDocumentTreeFragment::class.java.name) - } + supportFragmentManager.commit { add(android.R.id.content, AddDocumentTreeFragment()) } } } } diff --git a/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentTreeFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentTreeFragment.kt index d95630a8a..83b6a1bf4 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentTreeFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/AddDocumentTreeFragment.kt @@ -8,14 +8,10 @@ package me.zhanghai.android.files.storage import android.net.Uri import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.StringRes import androidx.fragment.app.Fragment -import kotlinx.parcelize.Parcelize import me.zhanghai.android.files.file.DocumentTreeUri import me.zhanghai.android.files.file.asDocumentTreeUriOrNull import me.zhanghai.android.files.file.takePersistablePermission -import me.zhanghai.android.files.util.ParcelableArgs -import me.zhanghai.android.files.util.args import me.zhanghai.android.files.util.finish import me.zhanghai.android.files.util.launchSafe @@ -24,13 +20,11 @@ class AddDocumentTreeFragment : Fragment() { ActivityResultContracts.OpenDocumentTree(), this::onOpenDocumentTreeResult ) - private val args by args() - override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) if (savedInstanceState == null) { - openDocumentTreeLauncher.launchSafe(args.treeUri?.value, this) + openDocumentTreeLauncher.launchSafe(null, this) } } @@ -44,13 +38,7 @@ class AddDocumentTreeFragment : Fragment() { private fun addDocumentTree(treeUri: DocumentTreeUri) { treeUri.takePersistablePermission() - val documentTree = DocumentTree(null, args.customNameRes?.let { getString(it) }, treeUri) + val documentTree = DocumentTree(null, null, treeUri) Storages.addOrReplace(documentTree) } - - @Parcelize - class Args( - @StringRes val customNameRes: Int?, - val treeUri: DocumentTreeUri? - ) : ParcelableArgs } diff --git a/app/src/main/java/me/zhanghai/android/files/storage/AddStorageDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/AddStorageDialogFragment.kt index 3baf24178..724d44f90 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/AddStorageDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/AddStorageDialogFragment.kt @@ -12,7 +12,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import me.zhanghai.android.files.R -import me.zhanghai.android.files.file.asDocumentTreeUri +import me.zhanghai.android.files.file.asDocumentUri import me.zhanghai.android.files.provider.document.resolver.ExternalStorageProviderHacks import me.zhanghai.android.files.util.createIntent import me.zhanghai.android.files.util.finish @@ -41,34 +41,29 @@ class AddStorageDialogFragment : AppCompatDialogFragment() { companion object { private val STORAGE_TYPES = listOfNotNull( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - (R.string.storage_add_storage_android_data - to AddDocumentTreeActivity ::class.createIntent() - .putArgs( - AddDocumentTreeFragment.Args( + R.string.storage_add_storage_android_data to + AddDocumentManagerShortcutActivity ::class.createIntent().putArgs( + AddDocumentManagerShortcutFragment.Args( R.string.storage_add_storage_android_data, - ExternalStorageProviderHacks.DOCUMENT_URI_ANDROID_DATA - .asDocumentTreeUri() + ExternalStorageProviderHacks.DOCUMENT_URI_ANDROID_DATA.asDocumentUri() ) - )) + ) } else null, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - (R.string.storage_add_storage_android_obb - to AddDocumentTreeActivity ::class.createIntent() - .putArgs( - AddDocumentTreeFragment.Args( + R.string.storage_add_storage_android_obb to + AddDocumentManagerShortcutActivity ::class.createIntent().putArgs( + AddDocumentManagerShortcutFragment.Args( R.string.storage_add_storage_android_obb, - ExternalStorageProviderHacks.DOCUMENT_URI_ANDROID_OBB - .asDocumentTreeUri() + ExternalStorageProviderHacks.DOCUMENT_URI_ANDROID_OBB.asDocumentUri() ) - )) + ) } else null, - R.string.storage_add_storage_document_tree - to AddDocumentTreeActivity::class.createIntent() - .putArgs(AddDocumentTreeFragment.Args(null, null)), - R.string.storage_add_storage_ftp_server to EditFtpServerActivity::class.createIntent() - .putArgs(EditFtpServerFragment.Args()), - R.string.storage_add_storage_sftp_server to EditSftpServerActivity::class.createIntent() - .putArgs(EditSftpServerFragment.Args()), + R.string.storage_add_storage_document_tree to + AddDocumentTreeActivity::class.createIntent(), + R.string.storage_add_storage_ftp_server to + EditFtpServerActivity::class.createIntent().putArgs(EditFtpServerFragment.Args()), + R.string.storage_add_storage_sftp_server to + EditSftpServerActivity::class.createIntent().putArgs(EditSftpServerFragment.Args()), R.string.storage_add_storage_smb_server to AddLanSmbServerActivity::class.createIntent() ) } diff --git a/app/src/main/java/me/zhanghai/android/files/storage/DocumentManagerShortcut.kt b/app/src/main/java/me/zhanghai/android/files/storage/DocumentManagerShortcut.kt new file mode 100644 index 000000000..526f540fd --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/storage/DocumentManagerShortcut.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.storage + +import android.content.Context +import android.content.Intent +import java8.nio.file.Path +import kotlinx.parcelize.Parcelize +import me.zhanghai.android.files.file.DocumentUri +import me.zhanghai.android.files.file.displayName +import me.zhanghai.android.files.util.createDocumentManagerViewDirectoryIntent +import me.zhanghai.android.files.util.createIntent +import me.zhanghai.android.files.util.putArgs +import kotlin.random.Random + +@Parcelize +data class DocumentManagerShortcut( + override val id: Long, + override val customName: String?, + val uri: DocumentUri +) : Storage() { + constructor( + id: Long?, + customName: String?, + uri: DocumentUri + ) : this(id ?: Random.nextLong(), customName, uri) + + override fun getDefaultName(context: Context): String = + uri.displayName ?: uri.value.lastPathSegment ?: uri.value.toString() + + override val description: String + get() = uri.value.toString() + + override val path: Path? + get() = null + + override fun createIntent(): Intent = uri.value.createDocumentManagerViewDirectoryIntent() + + override fun createEditIntent(): Intent = + EditDocumentManagerShortcutDialogActivity::class.createIntent() + .putArgs(EditDocumentManagerShortcutDialogFragment.Args(this)) +} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/DocumentTree.kt b/app/src/main/java/me/zhanghai/android/files/storage/DocumentTree.kt index 973a7930c..51b396dcc 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/DocumentTree.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/DocumentTree.kt @@ -51,7 +51,8 @@ data class DocumentTree( } override fun getDefaultName(context: Context): String = - uri.storageVolume?.getDescriptionCompat(context) ?: uri.displayName ?: uri.value.toString() + uri.storageVolume?.getDescriptionCompat(context) ?: uri.displayName + ?: uri.value.lastPathSegment ?: uri.value.toString() override val description: String get() = uri.value.toString() diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogActivity.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogActivity.kt new file mode 100644 index 000000000..fb572849b --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogActivity.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.storage + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.commit +import me.zhanghai.android.files.app.AppActivity +import me.zhanghai.android.files.util.args +import me.zhanghai.android.files.util.putArgs + +class EditDocumentManagerShortcutDialogActivity : AppActivity() { + private val args by args() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Calls ensureSubDecor(). + findViewById(android.R.id.content) + if (savedInstanceState == null) { + val fragment = EditDocumentManagerShortcutDialogFragment().putArgs(args) + supportFragmentManager.commit { + add(fragment, EditDocumentManagerShortcutDialogFragment::class.java.name) + } + } + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt new file mode 100644 index 000000000..c9b1eb5f6 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.storage + +import android.app.Dialog +import android.content.DialogInterface +import android.net.Uri +import android.os.Bundle +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatDialogFragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import kotlinx.parcelize.Parcelize +import me.zhanghai.android.files.R +import me.zhanghai.android.files.databinding.EditDocumentManagerShortcutDialogBinding +import me.zhanghai.android.files.file.asDocumentUriOrNull +import me.zhanghai.android.files.util.ParcelableArgs +import me.zhanghai.android.files.util.args +import me.zhanghai.android.files.util.finish +import me.zhanghai.android.files.util.hideTextInputLayoutErrorOnTextChange +import me.zhanghai.android.files.util.layoutInflater +import me.zhanghai.android.files.util.setTextWithSelection +import me.zhanghai.android.files.util.takeIfNotEmpty + +class EditDocumentManagerShortcutDialogFragment : AppCompatDialogFragment() { + private val args by args() + + private lateinit var binding: EditDocumentManagerShortcutDialogBinding + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + MaterialAlertDialogBuilder(requireContext(), theme) + .setTitle(R.string.storage_edit_document_tree_title) + .apply { + binding = EditDocumentManagerShortcutDialogBinding.inflate(context.layoutInflater) + val documentManagerShortcut = args.documentManagerShortcut + binding.nameLayout.placeholderText = + documentManagerShortcut.getDefaultName(binding.nameLayout.context) + binding.uriEdit.hideTextInputLayoutErrorOnTextChange(binding.uriLayout) + if (savedInstanceState == null) { + binding.nameEdit.setTextWithSelection( + documentManagerShortcut.getName(binding.nameEdit.context) + ) + binding.uriEdit.setText(documentManagerShortcut.uri.value.toString()) + } + setView(binding.root) + } + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.cancel() } + .setNeutralButton(R.string.remove) { _, _ -> remove() } + .create() + .apply { + window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + // Override the listener here so that we have control over when to close the dialog. + setOnShowListener { + getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { save() } + } + } + + private fun save() { + val documentManagerShortcut = getDocumentManagerShortcutOrSetError() ?: return + Storages.replace(documentManagerShortcut) + finish() + } + + private fun getDocumentManagerShortcutOrSetError(): DocumentManagerShortcut? { + var errorEdit: TextInputEditText? = null + val customName = binding.nameEdit.text.toString() + .takeIf { it.isNotEmpty() && it != binding.nameLayout.placeholderText } + val uriText = binding.uriEdit.text.toString().takeIfNotEmpty() + if (uriText == null) { + binding.uriLayout.error = + getString(R.string.storage_edit_document_manager_shortcut_uri_error_empty) + if (errorEdit == null) { + errorEdit = binding.uriEdit + } + } + val uri = Uri.parse(uriText).asDocumentUriOrNull() + if (uri == null) { + binding.uriLayout.error = + getString(R.string.storage_edit_document_manager_shortcut_uri_error_invalid) + if (errorEdit == null) { + errorEdit = binding.uriEdit + } + } + if (errorEdit != null) { + errorEdit.requestFocus() + return null + } + return DocumentManagerShortcut(args.documentManagerShortcut.id, customName, uri!!) + } + + private fun remove() { + Storages.remove(args.documentManagerShortcut) + finish() + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + finish() + } + + @Parcelize + class Args(val documentManagerShortcut: DocumentManagerShortcut) : ParcelableArgs +} diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentTreeDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentTreeDialogFragment.kt index ef9152a43..1b467b583 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentTreeDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentTreeDialogFragment.kt @@ -40,8 +40,7 @@ class EditDocumentTreeDialogFragment : AppCompatDialogFragment() { documentTree.getName(binding.nameEdit.context) ) } - val uri = documentTree.uri.value - binding.uriText.setText(uri.toString()) + binding.uriText.setText(documentTree.uri.value.toString()) val linuxPath = documentTree.linuxPath binding.pathLayout.isVisible = linuxPath != null binding.pathText.setText(linuxPath) diff --git a/app/src/main/java/me/zhanghai/android/files/storage/Storage.kt b/app/src/main/java/me/zhanghai/android/files/storage/Storage.kt index 1bb8e8f9b..4bb08c4d0 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/Storage.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/Storage.kt @@ -28,11 +28,13 @@ abstract class Storage : Parcelable { abstract val description: String - abstract val path: Path + abstract val path: Path? open val linuxPath: String? = null open val isVisible: Boolean = true + open fun createIntent(): Intent? = null + abstract fun createEditIntent(): Intent } diff --git a/app/src/main/java/me/zhanghai/android/files/util/IntentExtensions.kt b/app/src/main/java/me/zhanghai/android/files/util/IntentExtensions.kt index e48a5f8f6..95e7251c7 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/IntentExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/IntentExtensions.kt @@ -15,6 +15,7 @@ import androidx.core.app.ShareCompat import me.zhanghai.android.files.app.appClassLoader import me.zhanghai.android.files.app.application import me.zhanghai.android.files.app.packageManager +import me.zhanghai.android.files.compat.DocumentsContractCompat import me.zhanghai.android.files.compat.removeFlagsCompat import me.zhanghai.android.files.file.MimeType import me.zhanghai.android.files.file.intentType @@ -153,6 +154,10 @@ fun Collection.createSendStreamIntent(mimeTypes: Collection): Int removeFlagsCompat(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) } +fun Uri.createDocumentManagerViewDirectoryIntent(): Intent = + createViewIntent(MimeType.DIRECTORY) + .apply { DocumentsContractCompat.getDocumentsUiPackage()?.let { setPackage(it) } } + fun Uri.createViewIntent(): Intent = Intent(Intent.ACTION_VIEW, this) fun Uri.createViewIntent(mimeType: MimeType): Intent = diff --git a/app/src/main/res/layout/edit_document_manager_shortcut_dialog.xml b/app/src/main/res/layout/edit_document_manager_shortcut_dialog.xml new file mode 100644 index 000000000..0a60dbf85 --- /dev/null +++ b/app/src/main/res/layout/edit_document_manager_shortcut_dialog.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3037f94d4..752e0ed93 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -395,6 +395,10 @@ 编辑设备存储 名称 路径 + 添加 DocumentsUI 快捷方式 + 编辑 DocumentsUI 快捷方式 + 输入 URI + 无效 URI 添加外部存储 编辑外部存储 名称 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8794af750..1d0142815 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -395,6 +395,10 @@ 編輯裝置儲存空間 名稱 路徑 + 新增 DocumentsUI 捷徑 + 編輯 DocumentsUI 捷徑 + 輸入 URI + 無效的 URI 新增外部儲存空間 編輯外部儲存空間 名稱 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0887248a0..073b23f09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -513,6 +513,10 @@ Edit device storage Name Path + Add DocumentsUI shortcut + Edit DocumentsUI shortcut + Enter a URI + Invalid URI Add external storage Edit external storage Name From 7d35a195d061541aead79806946050c5c5bc91a9 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 19 Aug 2023 21:05:10 -0700 Subject: [PATCH 088/326] [Feature] Bump version to 1.6.0. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 54ca88d13..1f79355c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { // Not supporting notification runtime permission yet. //noinspection OldTargetApi targetSdk 32 - versionCode 31 - versionName '1.5.2' + versionCode 32 + versionName '1.6.0' resValue 'string', 'app_version', versionName + ' (' + versionCode + ')' buildConfigField 'String', 'FILE_PROVIDIER_AUTHORITY', 'APPLICATION_ID + ".file_provider"' resValue 'string', 'app_provider_authority', applicationId + '.app_provider' From 8af3a2281acf2d5580f8e77a1a32960557b08996 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 19 Aug 2023 21:49:26 -0700 Subject: [PATCH 089/326] [Feature] Add changelog for 1.6.0. Fixes: #988 --- fastlane/metadata/android/en-US/changelogs/32.txt | 9 +++++++++ fastlane/metadata/android/zh-CN/changelogs/32.txt | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/32.txt create mode 100644 fastlane/metadata/android/zh-CN/changelogs/32.txt diff --git a/fastlane/metadata/android/en-US/changelogs/32.txt b/fastlane/metadata/android/en-US/changelogs/32.txt new file mode 100644 index 000000000..6ac7259ee --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/32.txt @@ -0,0 +1,9 @@ +- Added grid view. +- Added per-app language setting. +- Added shortcut to DocumentsUI for accessing Android/data and Android/obb. +- Added URL information to FTP server notification. +- Added banner for Android TV. +- Fast scroll popup now shows text according to the current sort options. +- Video preview is now taken from 1/3 of the video instead of the first frame. +- Material Design 2 theme will be removed in the upcoming version 1.7.0 to ease code maintenance. +- Other bug fixes and improvements. diff --git a/fastlane/metadata/android/zh-CN/changelogs/32.txt b/fastlane/metadata/android/zh-CN/changelogs/32.txt new file mode 100644 index 000000000..52e6afc2a --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/32.txt @@ -0,0 +1,9 @@ +- 添加了网格视图。 +- 添加了分应用语言设置。 +- 添加了指向 DocumentsUI 的快捷方式以访问 Android/data 和 Android/obb。 +- 向 FTP 服务器的通知添加了 URL 信息。 +- 为 Android TV 添加了横幅。 +- 快速滚动的提示现在会根据排序选项来显示文字。 +- 视频预览现在会从视频的 1/3 处而非第一帧获取。 +- Material Design 2 主题将在未来的 1.7.0 版本中被移除以便于代码维护。 +- 其他错误修复和改进。 From 2dcfd45b574178ad47a679557fb3e61b0445d277 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 19 Aug 2023 22:43:20 -0700 Subject: [PATCH 090/326] [Fix] Fix wrong title for editing document manager shortcut. --- .../files/storage/EditDocumentManagerShortcutDialogFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt index c9b1eb5f6..d6136b1a1 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt @@ -33,7 +33,7 @@ class EditDocumentManagerShortcutDialogFragment : AppCompatDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = MaterialAlertDialogBuilder(requireContext(), theme) - .setTitle(R.string.storage_edit_document_tree_title) + .setTitle(R.string.storage_edit_document_manager_shortcut_title) .apply { binding = EditDocumentManagerShortcutDialogBinding.inflate(context.layoutInflater) val documentManagerShortcut = args.documentManagerShortcut From 137ec0e54b97ec12b613b61b40c644e370679c92 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 19 Aug 2023 22:48:59 -0700 Subject: [PATCH 091/326] [Feature] Update dependencies. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 1f79355c1..52d07824e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -123,7 +123,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-process:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$androidx_lifecycle_version" - implementation 'androidx.preference:preference-ktx:1.2.0' + implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'com.google.android.material:material:1.9.0' From a4e82ec6068c857c4a646e4129fa6656d6753898 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 19 Aug 2023 23:10:40 -0700 Subject: [PATCH 092/326] [Fix] Fix R8 for SMBJ and SMBJ-RPC. --- app/proguard-rules.pro | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 8cc37fa22..856753c46 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -56,6 +56,14 @@ -keep class org.bouncycastle.jcajce.provider.** { *; } -keep class org.bouncycastle.jce.provider.** { *; } +# SMBJ +-dontwarn javax.el.** +-dontwarn org.ietf.jgss.** +-dontwarn sun.security.x509.X509Key + +# SMBJ-RPC +-dontwarn java.rmi.UnmarshalException + # Stetho No-op # This library includes the no-op for stetho-okhttp3 which requires okhttp3, but we never used it. -dontwarn com.facebook.stetho.okhttp3.StethoInterceptor From 4d1abf584fe04b3881feb9a336fba23ff6f91aef Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 20 Aug 2023 01:38:42 -0700 Subject: [PATCH 093/326] [Fix] Fix lint errors. --- .../android/material/shape/MaterialShapeDrawableAccessor.java | 4 ++++ .../zhanghai/android/files/app/BackgroundActivityStarter.kt | 3 +++ .../android/files/util/ForegroundNotificationManager.kt | 3 +++ 3 files changed, 10 insertions(+) diff --git a/app/src/main/java/com/google/android/material/shape/MaterialShapeDrawableAccessor.java b/app/src/main/java/com/google/android/material/shape/MaterialShapeDrawableAccessor.java index 8890aff93..ed44d116f 100644 --- a/app/src/main/java/com/google/android/material/shape/MaterialShapeDrawableAccessor.java +++ b/app/src/main/java/com/google/android/material/shape/MaterialShapeDrawableAccessor.java @@ -5,6 +5,8 @@ package com.google.android.material.shape; +import android.annotation.SuppressLint; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -13,6 +15,7 @@ public class MaterialShapeDrawableAccessor { private MaterialShapeDrawableAccessor() {} + @SuppressLint("RestrictedApi") public static ElevationOverlayProvider getElevationOverlayProvider( @NonNull MaterialShapeDrawable drawable) { MaterialShapeDrawable.MaterialShapeDrawableState drawableState = @@ -20,6 +23,7 @@ public static ElevationOverlayProvider getElevationOverlayProvider( return drawableState.elevationOverlayProvider; } + @SuppressLint("RestrictedApi") public static void setElevationOverlayProvider( @NonNull MaterialShapeDrawable drawable, @Nullable ElevationOverlayProvider elevationOverlayProvider) { diff --git a/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt b/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt index 3f3392ca4..1637a4ea0 100644 --- a/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt +++ b/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt @@ -5,6 +5,7 @@ package me.zhanghai.android.files.app +import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -51,6 +52,8 @@ object BackgroundActivityStarter { Lifecycle.State.STARTED ) + // TODO: Add POST_NOTIFICATIONS permission when targeting API 33. + @SuppressLint("MissingPermission") private fun notifyStartActivity( intent: Intent, title: CharSequence, diff --git a/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt b/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt index 10af1587e..8fac77581 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt @@ -5,6 +5,7 @@ package me.zhanghai.android.files.util +import android.annotation.SuppressLint import android.app.Notification import android.app.Service import me.zhanghai.android.files.app.notificationManager @@ -14,6 +15,8 @@ class ForegroundNotificationManager(private val service: Service) { private var foregroundId = 0 + // TODO: Add POST_NOTIFICATIONS permission when targeting API 33. + @SuppressLint("MissingPermission") fun notify(id: Int, notification: Notification) { synchronized(notifications) { if (notifications.isEmpty()) { From 2ccf9fe57ffbdc1f16cf8a8b6ce0517e4d54e18f Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 20 Aug 2023 03:32:48 -0700 Subject: [PATCH 094/326] [Feature] Update Android CI. --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 298106b94..b3f6e69ed 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -11,7 +11,7 @@ jobs: - name: Check out repository uses: actions/checkout@v3 - name: Set up JDK 17 - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '17' From b7bf09c238a5368b25b8e1b512c1150938768e64 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 20 Aug 2023 04:02:46 -0700 Subject: [PATCH 095/326] [Feature] Update Shizuku-API. Rikka said it no longer requires coreLibraryDesugaring and the upgrade should just work. --- app/build.gradle | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 52d07824e..74e318717 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -165,9 +165,7 @@ dependencies { // dev.chrisbanesinsetter:insetter:0.6.0 makes inset unstable when entering immersive. implementation 'dev.chrisbanes:insetter-ktx:0.3.1' implementation 'dev.rikka.rikkax.preference:simplemenu-preference:1.0.3' - // Shizuku-API 13.1.0 requires core library desugaring. - //noinspection GradleDependency - implementation 'dev.rikka.shizuku:api:12.2.0' + implementation 'dev.rikka.shizuku:api:13.1.4' implementation ('eu.agno3.jcifs:jcifs-ng:2.1.9') { // org.bouncycastle:bcprov-jdk15on uses bytecode version unsupported by Jetifier, so use // org.bouncycastle:bcprov-jdk15to18 instead. From f654cdf231646a3443656a41019b36b0eb2a2223 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 20 Aug 2023 04:33:07 -0700 Subject: [PATCH 096/326] [Fix] More accurate wording in changelog. --- fastlane/metadata/android/en-US/changelogs/32.txt | 2 +- fastlane/metadata/android/zh-CN/changelogs/32.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/metadata/android/en-US/changelogs/32.txt b/fastlane/metadata/android/en-US/changelogs/32.txt index 6ac7259ee..0f4b517f2 100644 --- a/fastlane/metadata/android/en-US/changelogs/32.txt +++ b/fastlane/metadata/android/en-US/changelogs/32.txt @@ -4,6 +4,6 @@ - Added URL information to FTP server notification. - Added banner for Android TV. - Fast scroll popup now shows text according to the current sort options. -- Video preview is now taken from 1/3 of the video instead of the first frame. +- Video thumbnail is now taken from 1/3 of the video instead of the first frame. - Material Design 2 theme will be removed in the upcoming version 1.7.0 to ease code maintenance. - Other bug fixes and improvements. diff --git a/fastlane/metadata/android/zh-CN/changelogs/32.txt b/fastlane/metadata/android/zh-CN/changelogs/32.txt index 52e6afc2a..796540a47 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/32.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/32.txt @@ -4,6 +4,6 @@ - 向 FTP 服务器的通知添加了 URL 信息。 - 为 Android TV 添加了横幅。 - 快速滚动的提示现在会根据排序选项来显示文字。 -- 视频预览现在会从视频的 1/3 处而非第一帧获取。 +- 视频缩略图现在会从视频的 1/3 处而非第一帧获取。 - Material Design 2 主题将在未来的 1.7.0 版本中被移除以便于代码维护。 - 其他错误修复和改进。 From 7beff6b03744399c79cc09927248511d39621ff2 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 20 Aug 2023 05:25:17 -0700 Subject: [PATCH 097/326] [Fix] Remove bottom padding for edit document manager shortcut dialog. Because we already have app:errorEnabled="true" for the last text input layout. --- .../main/res/layout/edit_document_manager_shortcut_dialog.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/layout/edit_document_manager_shortcut_dialog.xml b/app/src/main/res/layout/edit_document_manager_shortcut_dialog.xml index 0a60dbf85..ec18f9093 100644 --- a/app/src/main/res/layout/edit_document_manager_shortcut_dialog.xml +++ b/app/src/main/res/layout/edit_document_manager_shortcut_dialog.xml @@ -24,7 +24,6 @@ android:paddingStart="?dialogPreferredPadding" android:paddingEnd="?dialogPreferredPadding" android:paddingTop="8dp" - android:paddingBottom="8dp" android:orientation="vertical"> Date: Sun, 20 Aug 2023 05:46:23 -0700 Subject: [PATCH 098/326] [Fix] Work around R8 bug. See https://issuetracker.google.com/issues/296654327 Fixes: #990 --- gradle.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gradle.properties b/gradle.properties index 3ef4f5785..d3222ccd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,9 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +# See https://issuetracker.google.com/issues/296654327 . +android.enableR8.fullMode=false + # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn From 31c996321bb27db7debeee9657b0a996e3c4c1ea Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 21 Aug 2023 19:06:12 -0700 Subject: [PATCH 099/326] [Feature] Update build tools. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 74e318717..5d1d16a6f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,7 +28,7 @@ android { namespace 'me.zhanghai.android.files' compileSdk 34 ndkVersion '25.2.9519653' - buildToolsVersion = '33.0.2' + buildToolsVersion = '34.0.0' defaultConfig { applicationId 'me.zhanghai.android.files' minSdk 21 From 1bd6c49391df209adf875462441cf4c0510d84b2 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 22 Aug 2023 03:56:49 -0700 Subject: [PATCH 100/326] [Feature] Always check out submodules if any. --- .github/workflows/android.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index b3f6e69ed..1b748a0d5 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -10,6 +10,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v3 + with: + submodules: true - name: Set up JDK 17 uses: actions/setup-java@v3 with: From 6e42c84e595d55477f11b43e02ede83663b782e8 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 22 Aug 2023 05:02:33 -0700 Subject: [PATCH 101/326] [Refactor] Rename syscalls to syscall. --- app/CMakeLists.txt | 4 +- .../permissions/GroupListLiveData.kt | 6 +- .../permissions/UserListLiveData.kt | 6 +- .../provider/common/FileChannelExtensions.kt | 4 +- .../files/provider/linux/LinuxCopyMove.kt | 56 +++++------ .../provider/linux/LinuxDirectoryStream.kt | 6 +- .../linux/LinuxUserPrincipalLookupService.kt | 10 +- .../linux/LocalLinuxFileAttributeView.kt | 32 +++---- .../provider/linux/LocalLinuxFileStore.kt | 22 ++--- .../linux/LocalLinuxFileSystemProvider.kt | 24 ++--- .../provider/linux/LocalLinuxWatchService.kt | 32 +++---- .../linux/syscall/{Syscalls.kt => Syscall.kt} | 4 +- .../linux/syscall/SyscallException.kt | 2 +- app/src/main/jni/{syscalls.c => syscall.c} | 96 +++++++++---------- 14 files changed, 152 insertions(+), 152 deletions(-) rename app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/{Syscalls.kt => Syscall.kt} (99%) rename app/src/main/jni/{syscalls.c => syscall.c} (93%) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 7ec0af964..815c17e26 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -5,5 +5,5 @@ project(MaterialFiles C) add_library(hiddenapi SHARED src/main/jni/hiddenapi.c) find_library(LOG_LIBRARY log) -add_library(syscalls SHARED src/main/jni/syscalls.c) -target_link_libraries(syscalls ${LOG_LIBRARY}) +add_library(syscall SHARED src/main/jni/syscall.c) +target_link_libraries(syscall ${LOG_LIBRARY}) diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListLiveData.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListLiveData.kt index c4398620b..0808beae5 100644 --- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListLiveData.kt +++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListLiveData.kt @@ -5,9 +5,9 @@ package me.zhanghai.android.files.fileproperties.permissions -import me.zhanghai.android.files.provider.linux.syscall.Syscalls.endgrent -import me.zhanghai.android.files.provider.linux.syscall.Syscalls.getgrent -import me.zhanghai.android.files.provider.linux.syscall.Syscalls.setgrent +import me.zhanghai.android.files.provider.linux.syscall.Syscall.endgrent +import me.zhanghai.android.files.provider.linux.syscall.Syscall.getgrent +import me.zhanghai.android.files.provider.linux.syscall.Syscall.setgrent class GroupListLiveData : PrincipalListLiveData() { override val androidPrincipals: MutableList diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListLiveData.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListLiveData.kt index 67a18f898..3cc3adca0 100644 --- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListLiveData.kt +++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListLiveData.kt @@ -5,9 +5,9 @@ package me.zhanghai.android.files.fileproperties.permissions -import me.zhanghai.android.files.provider.linux.syscall.Syscalls.endpwent -import me.zhanghai.android.files.provider.linux.syscall.Syscalls.getpwent -import me.zhanghai.android.files.provider.linux.syscall.Syscalls.setpwent +import me.zhanghai.android.files.provider.linux.syscall.Syscall.endpwent +import me.zhanghai.android.files.provider.linux.syscall.Syscall.getpwent +import me.zhanghai.android.files.provider.linux.syscall.Syscall.setpwent class UserListLiveData : PrincipalListLiveData() { override val androidPrincipals: MutableList diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/FileChannelExtensions.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/FileChannelExtensions.kt index aedb06113..7e4fc7e1a 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/FileChannelExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/FileChannelExtensions.kt @@ -10,7 +10,7 @@ import java8.nio.channels.FileChannel import java8.nio.channels.FileChannels import me.zhanghai.android.files.compat.NioUtilsCompat import me.zhanghai.android.files.provider.linux.syscall.SyscallException -import me.zhanghai.android.files.provider.linux.syscall.Syscalls +import me.zhanghai.android.files.provider.linux.syscall.Syscall import java.io.Closeable import java.io.FileDescriptor import java.io.IOException @@ -19,7 +19,7 @@ import kotlin.reflect.KClass fun KClass.open(fd: FileDescriptor, flags: Int): FileChannel { val closeable = Closeable { try { - Syscalls.close(fd) + Syscall.close(fd) } catch (e: SyscallException) { throw IOException(e) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxCopyMove.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxCopyMove.kt index 6b4d42fd3..9dc3f6a6d 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxCopyMove.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxCopyMove.kt @@ -15,7 +15,7 @@ import me.zhanghai.android.files.provider.common.toByteString import me.zhanghai.android.files.provider.linux.syscall.Constants import me.zhanghai.android.files.provider.linux.syscall.StructTimespec import me.zhanghai.android.files.provider.linux.syscall.SyscallException -import me.zhanghai.android.files.provider.linux.syscall.Syscalls +import me.zhanghai.android.files.provider.linux.syscall.Syscall import java.io.IOException import java.io.InterruptedIOException @@ -30,12 +30,12 @@ internal object LinuxCopyMove { throw UnsupportedOperationException(StandardCopyOption.ATOMIC_MOVE.toString()) } val sourceStat = try { - if (copyOptions.noFollowLinks) Syscalls.lstat(source) else Syscalls.stat(source) + if (copyOptions.noFollowLinks) Syscall.lstat(source) else Syscall.stat(source) } catch (e: SyscallException) { throw e.toFileSystemException(source.toString()) } val targetStat = try { - Syscalls.lstat(target) + Syscall.lstat(target) } catch (e: SyscallException) { if (e.errno != OsConstants.ENOENT) { throw e.toFileSystemException(target.toString()) @@ -56,7 +56,7 @@ internal object LinuxCopyMove { if (OsConstants.S_ISREG(sourceStat.st_mode)) { if (targetStat != null) { try { - Syscalls.remove(target) + Syscall.remove(target) } catch (e: SyscallException) { if (e.errno != OsConstants.ENOENT) { throw e.toFileSystemException(target.toString()) @@ -64,7 +64,7 @@ internal object LinuxCopyMove { } } val sourceFd = try { - Syscalls.open(source, OsConstants.O_RDONLY, 0) + Syscall.open(source, OsConstants.O_RDONLY, 0) } catch (e: SyscallException) { throw e.toFileSystemException(source.toString()) } @@ -75,7 +75,7 @@ internal object LinuxCopyMove { targetFlags = targetFlags or OsConstants.O_EXCL } val targetFd = try { - Syscalls.open(target, targetFlags, sourceStat.st_mode) + Syscall.open(target, targetFlags, sourceStat.st_mode) } catch (e: SyscallException) { e.maybeThrowInvalidFileNameException(target.toString()) throw e.toFileSystemException(target.toString()) @@ -88,7 +88,7 @@ internal object LinuxCopyMove { var copiedSize = 0L while (true) { val sentSize = try { - Syscalls.sendfile(targetFd, sourceFd, null, SEND_FILE_COUNT.toLong()) + Syscall.sendfile(targetFd, sourceFd, null, SEND_FILE_COUNT.toLong()) } catch (e: SyscallException) { throw e.toFileSystemException(source.toString(), target.toString()) } @@ -109,13 +109,13 @@ internal object LinuxCopyMove { successful = true } finally { try { - Syscalls.close(targetFd) + Syscall.close(targetFd) } catch (e: SyscallException) { throw e.toFileSystemException(target.toString()) } finally { if (!successful) { try { - Syscalls.remove(target) + Syscall.remove(target) } catch (e: SyscallException) { e.printStackTrace() } @@ -124,7 +124,7 @@ internal object LinuxCopyMove { } } finally { try { - Syscalls.close(sourceFd) + Syscall.close(sourceFd) } catch (e: SyscallException) { throw e.toFileSystemException(source.toString()) } @@ -132,7 +132,7 @@ internal object LinuxCopyMove { } else if (OsConstants.S_ISDIR(sourceStat.st_mode)) { if (targetStat != null) { try { - Syscalls.remove(target) + Syscall.remove(target) } catch (e: SyscallException) { if (e.errno != OsConstants.ENOENT) { throw e.toFileSystemException(target.toString()) @@ -140,7 +140,7 @@ internal object LinuxCopyMove { } } try { - Syscalls.mkdir(target, sourceStat.st_mode) + Syscall.mkdir(target, sourceStat.st_mode) } catch (e: SyscallException) { e.maybeThrowInvalidFileNameException(target.toString()) throw e.toFileSystemException(target.toString()) @@ -148,16 +148,16 @@ internal object LinuxCopyMove { copyOptions.progressListener?.invoke(sourceStat.st_size) } else if (OsConstants.S_ISLNK(sourceStat.st_mode)) { val sourceTarget = try { - Syscalls.readlink(source) + Syscall.readlink(source) } catch (e: SyscallException) { throw e.toFileSystemException(source.toString()) } try { - Syscalls.symlink(sourceTarget, target) + Syscall.symlink(sourceTarget, target) } catch (e: SyscallException) { if (e.errno == OsConstants.EEXIST && copyOptions.replaceExisting) { try { - Syscalls.remove(target) + Syscall.remove(target) } catch (e2: SyscallException) { if (e2.errno != OsConstants.ENOENT) { e2.addSuppressed(e.toFileSystemException(target.toString())) @@ -165,7 +165,7 @@ internal object LinuxCopyMove { } } try { - Syscalls.symlink(sourceTarget, target) + Syscall.symlink(sourceTarget, target) } catch (e2: SyscallException) { e2.addSuppressed(e.toFileSystemException(target.toString())) throw e2.toFileSystemException(target.toString()) @@ -184,14 +184,14 @@ internal object LinuxCopyMove { // setuid work properly. try { if (copyOptions.copyAttributes) { - Syscalls.lchown(target, sourceStat.st_uid, sourceStat.st_gid) + Syscall.lchown(target, sourceStat.st_uid, sourceStat.st_gid) } } catch (e: SyscallException) { e.printStackTrace() } try { if (!OsConstants.S_ISLNK(sourceStat.st_mode)) { - Syscalls.chmod(target, sourceStat.st_mode) + Syscall.chmod(target, sourceStat.st_mode) } } catch (e: SyscallException) { e.printStackTrace() @@ -205,19 +205,19 @@ internal object LinuxCopyMove { StructTimespec(0, Constants.UTIME_OMIT) }, sourceStat.st_mtim ) - Syscalls.lutimens(target, times) + Syscall.lutimens(target, times) } catch (e: SyscallException) { e.printStackTrace() } try { // TODO: Allow u+rw temporarily if we are to copy xattrs. - val xattrNames = Syscalls.llistxattr(source) + val xattrNames = Syscall.llistxattr(source) for (xattrName in xattrNames) { if (!(copyOptions.copyAttributes || xattrName.startsWith(XATTR_NAME_PREFIX_USER))) { continue } - val xattrValue = Syscalls.lgetxattr(target, xattrName) - Syscalls.lsetxattr(target, xattrName, xattrValue, 0) + val xattrValue = Syscall.lgetxattr(target, xattrName) + Syscall.lsetxattr(target, xattrName, xattrValue, 0) } } catch (e: SyscallException) { e.printStackTrace() @@ -234,12 +234,12 @@ internal object LinuxCopyMove { @Throws(IOException::class) fun move(source: ByteString, target: ByteString, copyOptions: CopyOptions) { val sourceStat = try { - Syscalls.lstat(source) + Syscall.lstat(source) } catch (e: SyscallException) { throw e.toFileSystemException(source.toString()) } val targetStat = try { - Syscalls.lstat(target) + Syscall.lstat(target) } catch (e: SyscallException) { if (e.errno != OsConstants.ENOENT) { throw e.toFileSystemException(target.toString()) @@ -256,14 +256,14 @@ internal object LinuxCopyMove { throw FileAlreadyExistsException(source.toString(), target.toString(), null) } try { - Syscalls.remove(target) + Syscall.remove(target) } catch (e: SyscallException) { throw e.toFileSystemException(target.toString()) } } var renameSuccessful = false try { - Syscalls.rename(source, target) + Syscall.rename(source, target) renameSuccessful = true } catch (e: SyscallException) { if (copyOptions.atomicMove) { @@ -289,11 +289,11 @@ internal object LinuxCopyMove { } copy(source, target, copyOptions) try { - Syscalls.remove(source) + Syscall.remove(source) } catch (e: SyscallException) { if (e.errno != OsConstants.ENOENT) { try { - Syscalls.remove(target) + Syscall.remove(target) } catch (e2: SyscallException) { e.addSuppressed(e2.toFileSystemException(target.toString())) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxDirectoryStream.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxDirectoryStream.kt index ce21cf780..b119291a7 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxDirectoryStream.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxDirectoryStream.kt @@ -10,7 +10,7 @@ import java8.nio.file.DirectoryStream import java8.nio.file.Path import me.zhanghai.android.files.provider.common.toByteString import me.zhanghai.android.files.provider.linux.syscall.SyscallException -import me.zhanghai.android.files.provider.linux.syscall.Syscalls +import me.zhanghai.android.files.provider.linux.syscall.Syscall import java.io.IOException internal class LinuxDirectoryStream( @@ -42,7 +42,7 @@ internal class LinuxDirectoryStream( return } try { - Syscalls.closedir(dir) + Syscall.closedir(dir) } catch (e: SyscallException) { throw e.toFileSystemException(directory.toString()) } @@ -81,7 +81,7 @@ internal class LinuxDirectoryStream( return null } val dirent = try { - Syscalls.readdir(dir) + Syscall.readdir(dir) } catch (e: SyscallException) { throw DirectoryIteratorException( e.toFileSystemException(directory.toString()) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxUserPrincipalLookupService.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxUserPrincipalLookupService.kt index e6813420f..a603d7cac 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxUserPrincipalLookupService.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxUserPrincipalLookupService.kt @@ -12,7 +12,7 @@ import me.zhanghai.android.files.provider.common.PosixGroup import me.zhanghai.android.files.provider.common.PosixUser import me.zhanghai.android.files.provider.common.toByteString import me.zhanghai.android.files.provider.linux.syscall.SyscallException -import me.zhanghai.android.files.provider.linux.syscall.Syscalls +import me.zhanghai.android.files.provider.linux.syscall.Syscall import java.io.IOException internal object LinuxUserPrincipalLookupService : UserPrincipalLookupService() { @@ -23,7 +23,7 @@ internal object LinuxUserPrincipalLookupService : UserPrincipalLookupService() { @Throws(IOException::class) fun lookupPrincipalByName(name: ByteString): PosixUser { val passwd = try { - Syscalls.getpwnam(name) + Syscall.getpwnam(name) } catch (e: SyscallException) { throw e.toFileSystemException(null) } ?: throw UserPrincipalNotFoundException(name.toString()) @@ -40,7 +40,7 @@ internal object LinuxUserPrincipalLookupService : UserPrincipalLookupService() { @Throws(SyscallException::class) fun getUserById(uid: Int): PosixUser { - val passwd = Syscalls.getpwuid(uid) + val passwd = Syscall.getpwuid(uid) return PosixUser(uid, passwd?.pw_name) } @@ -51,7 +51,7 @@ internal object LinuxUserPrincipalLookupService : UserPrincipalLookupService() { @Throws(IOException::class) fun lookupPrincipalByGroupName(group: ByteString): PosixGroup { val groupStruct = try { - Syscalls.getgrnam(group) + Syscall.getgrnam(group) } catch (e: SyscallException) { throw e.toFileSystemException(null) } ?: throw UserPrincipalNotFoundException(group.toString()) @@ -68,7 +68,7 @@ internal object LinuxUserPrincipalLookupService : UserPrincipalLookupService() { @Throws(SyscallException::class) fun getGroupById(gid: Int): PosixGroup { - val group = Syscalls.getgrgid(gid) + val group = Syscall.getgrgid(gid) return PosixGroup(gid, group?.gr_name) } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileAttributeView.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileAttributeView.kt index 9dc6c9ad7..d8b3c155d 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileAttributeView.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileAttributeView.kt @@ -16,7 +16,7 @@ import me.zhanghai.android.files.provider.common.toInt import me.zhanghai.android.files.provider.linux.syscall.Constants import me.zhanghai.android.files.provider.linux.syscall.StructTimespec import me.zhanghai.android.files.provider.linux.syscall.SyscallException -import me.zhanghai.android.files.provider.linux.syscall.Syscalls +import me.zhanghai.android.files.provider.linux.syscall.Syscall import java.io.IOException internal class LocalLinuxFileAttributeView( @@ -29,9 +29,9 @@ internal class LocalLinuxFileAttributeView( override fun readAttributes(): LinuxFileAttributes { val stat = try { if (noFollowLinks) { - Syscalls.lstat(path) + Syscall.lstat(path) } else { - Syscalls.stat(path) + Syscall.stat(path) } } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) @@ -52,9 +52,9 @@ internal class LocalLinuxFileAttributeView( } val seLinuxContext = try { if (noFollowLinks) { - Syscalls.lgetfilecon(path) + Syscall.lgetfilecon(path) } else { - Syscalls.getfilecon(path) + Syscall.getfilecon(path) } } catch (e: SyscallException) { // SELinux calls may fail with ENODATA or ENOTSUP, and there may be other errors. @@ -81,9 +81,9 @@ internal class LocalLinuxFileAttributeView( val times = arrayOf(lastAccessTime.toTimespec(), lastModifiedTime.toTimespec()) try { if (noFollowLinks) { - Syscalls.lutimens(path, times) + Syscall.lutimens(path, times) } else { - Syscalls.utimens(path, times) + Syscall.utimens(path, times) } } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) @@ -103,9 +103,9 @@ internal class LocalLinuxFileAttributeView( val uid = owner.id try { if (noFollowLinks) { - Syscalls.lchown(path, uid, -1) + Syscall.lchown(path, uid, -1) } else { - Syscalls.chown(path, uid, -1) + Syscall.chown(path, uid, -1) } } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) @@ -117,9 +117,9 @@ internal class LocalLinuxFileAttributeView( val gid = group.id try { if (noFollowLinks) { - Syscalls.lchown(path, -1, gid) + Syscall.lchown(path, -1, gid) } else { - Syscalls.chown(path, -1, gid) + Syscall.chown(path, -1, gid) } } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) @@ -133,7 +133,7 @@ internal class LocalLinuxFileAttributeView( } val modeInt = mode.toInt() try { - Syscalls.chmod(path, modeInt) + Syscall.chmod(path, modeInt) } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) } @@ -143,9 +143,9 @@ internal class LocalLinuxFileAttributeView( override fun setSeLinuxContext(context: ByteString) { try { if (noFollowLinks) { - Syscalls.lsetfilecon(path, context) + Syscall.lsetfilecon(path, context) } else { - Syscalls.setfilecon(path, context) + Syscall.setfilecon(path, context) } } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) @@ -158,13 +158,13 @@ internal class LocalLinuxFileAttributeView( path } else { try { - Syscalls.realpath(path) + Syscall.realpath(path) } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) } } try { - Syscalls.selinux_android_restorecon(path, 0) + Syscall.selinux_android_restorecon(path, 0) } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileStore.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileStore.kt index dd7c8ef0f..4b09679e9 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileStore.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileStore.kt @@ -19,7 +19,7 @@ import me.zhanghai.android.files.provider.linux.syscall.Constants import me.zhanghai.android.files.provider.linux.syscall.Int32Ref import me.zhanghai.android.files.provider.linux.syscall.StructMntent import me.zhanghai.android.files.provider.linux.syscall.SyscallException -import me.zhanghai.android.files.provider.linux.syscall.Syscalls +import me.zhanghai.android.files.provider.linux.syscall.Syscall import me.zhanghai.android.files.util.andInv import me.zhanghai.android.files.util.hasBits import me.zhanghai.android.files.util.readParcelable @@ -73,7 +73,7 @@ internal class LocalLinuxFileStore : PosixFileStore, Parcelable { override fun type(): String = mntent.mnt_type.toString() - override fun isReadOnly(): Boolean = Syscalls.hasmntopt(mntent, OPTION_RO) + override fun isReadOnly(): Boolean = Syscall.hasmntopt(mntent, OPTION_RO) @Throws(IOException::class) override fun setReadOnly(readOnly: Boolean) { @@ -124,7 +124,7 @@ internal class LocalLinuxFileStore : PosixFileStore, Parcelable { ) { val mountFlags = mountFlags or Constants.MS_REMOUNT try { - Syscalls.mount(source, target, fileSystemType, mountFlags, data) + Syscall.mount(source, target, fileSystemType, mountFlags, data) } catch (e: SyscallException) { val readOnly = mountFlags.hasBits(Constants.MS_RDONLY) val isReadOnlyError = e.errno == OsConstants.EACCES || e.errno == OsConstants.EROFS @@ -132,13 +132,13 @@ internal class LocalLinuxFileStore : PosixFileStore, Parcelable { throw e } try { - val fd = Syscalls.open(source!!, OsConstants.O_RDONLY, 0) + val fd = Syscall.open(source!!, OsConstants.O_RDONLY, 0) try { - Syscalls.ioctl_int(fd, Constants.BLKROSET, Int32Ref(0)) + Syscall.ioctl_int(fd, Constants.BLKROSET, Int32Ref(0)) } finally { - Syscalls.close(fd) + Syscall.close(fd) } - Syscalls.mount(source, target, fileSystemType, mountFlags, data) + Syscall.mount(source, target, fileSystemType, mountFlags, data) } catch (e2: SyscallException) { e.addSuppressed(e2) throw e @@ -167,7 +167,7 @@ internal class LocalLinuxFileStore : PosixFileStore, Parcelable { @Throws(IOException::class) private fun getStatVfs(): StructStatVfs = try { - Syscalls.statvfs(path.toByteString()) + Syscall.statvfs(path.toByteString()) } catch (e: SyscallException) { throw e.toFileSystemException(path.toString()) } @@ -273,14 +273,14 @@ internal class LocalLinuxFileStore : PosixFileStore, Parcelable { @Throws(SyscallException::class) private fun getMountEntries(): List { val entries = mutableListOf() - val file = Syscalls.setmntent(PATH_PROC_SELF_MOUNTS, MODE_R) + val file = Syscall.setmntent(PATH_PROC_SELF_MOUNTS, MODE_R) try { while (true) { - val mntent = Syscalls.getmntent(file) ?: break + val mntent = Syscall.getmntent(file) ?: break entries += mntent } } finally { - Syscalls.endmntent(file) + Syscall.endmntent(file) } return entries } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileSystemProvider.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileSystemProvider.kt index 0250228be..dc2655b4c 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileSystemProvider.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxFileSystemProvider.kt @@ -40,7 +40,7 @@ import me.zhanghai.android.files.provider.common.toLinkOptions import me.zhanghai.android.files.provider.common.toOpenOptions import me.zhanghai.android.files.provider.linux.media.MediaScanner import me.zhanghai.android.files.provider.linux.syscall.SyscallException -import me.zhanghai.android.files.provider.linux.syscall.Syscalls +import me.zhanghai.android.files.provider.linux.syscall.Syscall import me.zhanghai.android.files.util.hasBits import java.io.IOException import java.net.URI @@ -86,7 +86,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst val mode = (PosixFileMode.fromAttributes(attributes) ?: PosixFileMode.CREATE_FILE_DEFAULT) .toInt() val fd = try { - Syscalls.open(fileBytes, flags, mode) + Syscall.open(fileBytes, flags, mode) } catch (e: SyscallException) { if (flags.hasBits(OsConstants.O_CREAT)) { e.maybeThrowInvalidFileNameException(fileBytes.toString()) @@ -96,7 +96,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst val fileChannel = FileChannel::class.open(fd, flags) if (openOptions.deleteOnClose) { try { - Syscalls.remove(fileBytes) + Syscall.remove(fileBytes) } catch (e: SyscallException) { e.printStackTrace() } @@ -121,7 +121,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst directory as? LinuxPath ?: throw ProviderMismatchException(directory.toString()) val directoryBytes = directory.toByteString() val dir = try { - Syscalls.opendir(directoryBytes) + Syscall.opendir(directoryBytes) } catch (e: SyscallException) { throw e.toFileSystemException(directoryBytes.toString()) } @@ -135,7 +135,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst val mode = (PosixFileMode.fromAttributes(attributes) ?: PosixFileMode.CREATE_DIRECTORY_DEFAULT).toInt() try { - Syscalls.mkdir(directoryBytes, mode) + Syscall.mkdir(directoryBytes, mode) } catch (e: SyscallException) { e.maybeThrowInvalidFileNameException(directoryBytes.toString()) throw e.toFileSystemException(directoryBytes.toString()) @@ -156,7 +156,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst } val linkBytes = link.toByteString() try { - Syscalls.symlink(targetBytes, linkBytes) + Syscall.symlink(targetBytes, linkBytes) } catch (e: SyscallException) { e.maybeThrowInvalidFileNameException(linkBytes.toString()) throw e.toFileSystemException(linkBytes.toString(), targetBytes.toString()) @@ -171,7 +171,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst val oldPathBytes = existing.toByteString() val newPathBytes = link.toByteString() try { - Syscalls.link(oldPathBytes, newPathBytes) + Syscall.link(oldPathBytes, newPathBytes) } catch (e: SyscallException) { e.maybeThrowInvalidFileNameException(newPathBytes.toString()) throw e.toFileSystemException(newPathBytes.toString(), oldPathBytes.toString()) @@ -184,7 +184,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst path as? LinuxPath ?: throw ProviderMismatchException(path.toString()) val pathBytes = path.toByteString() try { - Syscalls.remove(pathBytes) + Syscall.remove(pathBytes) } catch (e: SyscallException) { throw e.toFileSystemException(pathBytes.toString()) } @@ -196,7 +196,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst link as? LinuxPath ?: throw ProviderMismatchException(link.toString()) val linkBytes = link.toByteString() val targetBytes = try { - Syscalls.readlink(linkBytes) + Syscall.readlink(linkBytes) } catch (e: SyscallException) { e.maybeThrowNotLinkException(linkBytes.toString()) throw e.toFileSystemException(linkBytes.toString()) @@ -240,12 +240,12 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst val pathBytes = path.toByteString() val path2Bytes = path2.toByteString() val pathStat = try { - Syscalls.lstat(pathBytes) + Syscall.lstat(pathBytes) } catch (e: SyscallException) { throw e.toFileSystemException(pathBytes.toString()) } val path2Stat = try { - Syscalls.lstat(path2Bytes) + Syscall.lstat(path2Bytes) } catch (e: SyscallException) { throw e.toFileSystemException(path2Bytes.toString()) } @@ -287,7 +287,7 @@ class LocalLinuxFileSystemProvider(provider: LinuxFileSystemProvider) : FileSyst } val accessible = try { // TODO: Should use euidaccess() but that's unavailable on Android. - Syscalls.access(pathBytes, mode) + Syscall.access(pathBytes, mode) } catch (e: SyscallException) { throw e.toFileSystemException(pathBytes.toString()) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxWatchService.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxWatchService.kt index 166fe7d2c..81995d991 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxWatchService.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/LocalLinuxWatchService.kt @@ -19,7 +19,7 @@ import me.zhanghai.android.files.provider.common.AbstractWatchService import me.zhanghai.android.files.provider.common.readAttributes import me.zhanghai.android.files.provider.linux.syscall.Constants import me.zhanghai.android.files.provider.linux.syscall.SyscallException -import me.zhanghai.android.files.provider.linux.syscall.Syscalls +import me.zhanghai.android.files.provider.linux.syscall.Syscall import me.zhanghai.android.files.util.hasBits import java.io.Closeable import java.io.FileDescriptor @@ -91,14 +91,14 @@ internal class LocalLinuxWatchService : AbstractWatchService init { isDaemon = true try { - socketFds = Syscalls.socketpair(OsConstants.AF_UNIX, OsConstants.SOCK_STREAM, 0) - val flags = Syscalls.fcntl(socketFds[0], OsConstants.F_GETFL) + socketFds = Syscall.socketpair(OsConstants.AF_UNIX, OsConstants.SOCK_STREAM, 0) + val flags = Syscall.fcntl(socketFds[0], OsConstants.F_GETFL) if (!flags.hasBits(OsConstants.O_NONBLOCK)) { - Syscalls.fcntl( + Syscall.fcntl( socketFds[0], OsConstants.F_SETFL, flags or OsConstants.O_NONBLOCK ) } - inotifyFd = Syscalls.inotify_init1(OsConstants.O_NONBLOCK) + inotifyFd = Syscall.inotify_init1(OsConstants.O_NONBLOCK) } catch (e: SyscallException) { throw e.toFileSystemException(null) } @@ -115,7 +115,7 @@ internal class LocalLinuxWatchService : AbstractWatchService var mask = eventKindsToMask(kinds) mask = maybeAddDontFollowMask(path, mask) val wd = try { - Syscalls.inotify_add_watch(inotifyFd, pathBytes, mask) + Syscall.inotify_add_watch(inotifyFd, pathBytes, mask) } catch (e: SyscallException) { continuation.resumeWithException( e.toFileSystemException(pathBytes.toString()) @@ -161,7 +161,7 @@ internal class LocalLinuxWatchService : AbstractWatchService if (key.isValid) { val wd = key.watchDescriptor try { - Syscalls.inotify_rm_watch(inotifyFd, wd) + Syscall.inotify_rm_watch(inotifyFd, wd) } catch (e: SyscallException) { e.toFileSystemException(key.watchable().toString()) .printStackTrace() @@ -191,7 +191,7 @@ internal class LocalLinuxWatchService : AbstractWatchService for (key in keys.values) { val wd = key.watchDescriptor try { - Syscalls.inotify_rm_watch(inotifyFd, wd) + Syscall.inotify_rm_watch(inotifyFd, wd) } catch (e: SyscallException) { continuation.resumeWithException( e.toFileSystemException(key.watchable().toString()) @@ -202,9 +202,9 @@ internal class LocalLinuxWatchService : AbstractWatchService } keys.clear() try { - Syscalls.close(inotifyFd) - Syscalls.close(socketFds[1]) - Syscalls.close(socketFds[0]) + Syscall.close(inotifyFd) + Syscall.close(socketFds[1]) + Syscall.close(socketFds[0]) } catch (e: SyscallException) { e.printStackTrace() } @@ -234,7 +234,7 @@ internal class LocalLinuxWatchService : AbstractWatchService } } try { - Syscalls.write(socketFds[1], ONE_BYTE) + Syscall.write(socketFds[1], ONE_BYTE) } catch (e: InterruptedIOException) { continuation.resumeWithException(e) } catch (e: SyscallException) { @@ -248,10 +248,10 @@ internal class LocalLinuxWatchService : AbstractWatchService while (true) { fds[0].revents = 0 fds[1].revents = 0 - Syscalls.poll(fds, -1) + Syscall.poll(fds, -1) if (fds[0].revents.toInt().hasBits(OsConstants.POLLIN)) { val size = try { - Syscalls.read(socketFds[0], ONE_BYTE) + Syscall.read(socketFds[0], ONE_BYTE) } catch (e: SyscallException) { if (e.errno != OsConstants.EAGAIN) { throw e @@ -272,7 +272,7 @@ internal class LocalLinuxWatchService : AbstractWatchService } if (fds[1].revents.toInt().hasBits(OsConstants.POLLIN)) { val size = try { - Syscalls.read(inotifyFd, inotifyBuffer) + Syscall.read(inotifyFd, inotifyBuffer) } catch (e: SyscallException) { if (e.errno != OsConstants.EAGAIN) { throw e @@ -286,7 +286,7 @@ internal class LocalLinuxWatchService : AbstractWatchService } continue } - val events = Syscalls.inotify_get_events(inotifyBuffer, 0, size) + val events = Syscall.inotify_get_events(inotifyBuffer, 0, size) for (event in events) { if (event.mask.hasBits(Constants.IN_Q_OVERFLOW)) { for (key in keys.values) { diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscalls.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscall.kt similarity index 99% rename from app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscalls.kt rename to app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscall.kt index 3ee1191c5..285480a4f 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscalls.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscall.kt @@ -20,9 +20,9 @@ import me.zhanghai.android.libselinux.SeLinux import java.io.FileDescriptor import java.io.InterruptedIOException -object Syscalls { +object Syscall { init { - System.loadLibrary("syscalls") + System.loadLibrary("syscall") } @Throws(SyscallException::class) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/SyscallException.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/SyscallException.kt index 95c42a79b..ada62c3c6 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/SyscallException.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/SyscallException.kt @@ -70,6 +70,6 @@ class SyscallException @JvmOverloads constructor( companion object { private fun perror(errno: Int, functionName: String): String = - "$functionName: ${Syscalls.strerror(errno)}" + "$functionName: ${Syscall.strerror(errno)}" } } diff --git a/app/src/main/jni/syscalls.c b/app/src/main/jni/syscall.c similarity index 93% rename from app/src/main/jni/syscalls.c rename to app/src/main/jni/syscall.c index fa2150ed1..3a8cbd28f 100644 --- a/app/src/main/jni/syscalls.c +++ b/app/src/main/jni/syscall.c @@ -32,7 +32,7 @@ #define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) -#define LOG_TAG "syscalls" +#define LOG_TAG "syscall" #undef TEMP_FAILURE_RETRY // Checks errno when return value is -1. @@ -372,7 +372,7 @@ static jobject newFileDescriptor(JNIEnv *env, int fd) { } JNIEXPORT jboolean JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_access( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_access( JNIEnv *env, jclass clazz, jobject javaPath, jint javaMode) { char *path = mallocStringFromByteString(env, javaPath); int mode = javaMode; @@ -387,7 +387,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_access( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_chmod( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_chmod( JNIEnv *env, jclass clazz, jobject javaPath, jint javaMode) { char *path = mallocStringFromByteString(env, javaPath); mode_t mode = (mode_t) javaMode; @@ -399,7 +399,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_chmod( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_chown( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_chown( JNIEnv *env, jclass clazz, jobject javaPath, jint javaUid, jint javaGid) { char *path = mallocStringFromByteString(env, javaPath); uid_t uid = (uid_t) javaUid; @@ -412,7 +412,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_chown( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_closedir( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_closedir( JNIEnv *env, jclass clazz, jlong javaDir) { DIR *dir = (DIR *) javaDir; TEMP_FAILURE_RETRY(closedir(dir)); @@ -422,7 +422,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_closedir( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_endmntent( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_endmntent( JNIEnv *env, jclass clazz, jlong javaFile) { FILE *file = (FILE *) javaFile; // The endmntent() function always returns 1. @@ -461,7 +461,7 @@ void endgrent() { #endif JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_endgrent(JNIEnv *env, jclass clazz) { +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_endgrent(JNIEnv *env, jclass clazz) { TEMP_FAILURE_RETRY_V(endgrent()); if (errno) { throwSyscallException(env, "endgrent"); @@ -495,7 +495,7 @@ void endpwent() { #endif JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_endpwent(JNIEnv *env, jclass clazz) { +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_endpwent(JNIEnv *env, jclass clazz) { TEMP_FAILURE_RETRY_V(endpwent()); if (errno) { throwSyscallException(env, "endpwent"); @@ -503,13 +503,13 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_endpwent(JNIEnv * } JNIEXPORT jint JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_errno( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_errno( JNIEnv *env, jclass clazz) { return errno; } JNIEXPORT jint JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_fcntl_1int( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_fcntl_1int( JNIEnv *env, jclass clazz, jobject javaFd, jint javaCmd, jint javaArg) { int fd = getFdFromFileDescriptor(env, javaFd); int cmd = javaCmd; @@ -523,7 +523,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_fcntl_1int( } JNIEXPORT jint JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_fcntl_1void( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_fcntl_1void( JNIEnv *env, jclass clazz, jobject javaFd, jint javaCmd) { int fd = getFdFromFileDescriptor(env, javaFd); int cmd = javaCmd; @@ -590,7 +590,7 @@ static jobject newStructGroup(JNIEnv *env, const struct group *group) { } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getgrent(JNIEnv *env, jclass clazz) { +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_getgrent(JNIEnv *env, jclass clazz) { while (true) { // getgrent() in bionic is thread safe. struct group *group = TEMP_FAILURE_RETRY_N(getgrent()); @@ -618,7 +618,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getgrent(JNIEnv * } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getgrgid( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_getgrgid( JNIEnv *env, jclass clazz, jint javaGid) { #if __ANDROID_API__ >= __ANDROID_API_N__ gid_t gid = (gid_t) javaGid; @@ -655,7 +655,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getgrgid( } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getgrnam( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_getgrnam( JNIEnv *env, jclass clazz, jobject javaName) { #if __ANDROID_API__ >= __ANDROID_API_N__ char *name = mallocStringFromByteString(env, javaName); @@ -751,7 +751,7 @@ static jobject newStructMntent(JNIEnv *env, const struct mntent *mntent) { } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getmntent( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_getmntent( JNIEnv *env, jclass clazz, jlong javaFile) { FILE *file = (FILE *) javaFile; #if __ANDROID_API__ >= __ANDROID_API_L_MR1__ @@ -830,7 +830,7 @@ static jobject newStructPasswd(JNIEnv *env, const struct passwd *passwd) { } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getpwent(JNIEnv *env, jclass clazz) { +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_getpwent(JNIEnv *env, jclass clazz) { while (true) { // getpwent() in bionic is thread safe. struct passwd *passwd = TEMP_FAILURE_RETRY_N(getpwent()); @@ -853,7 +853,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getpwent(JNIEnv * } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getpwnam( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_getpwnam( JNIEnv *env, jclass clazz, jobject javaName) { char *name = mallocStringFromByteString(env, javaName); size_t bufferSize = (size_t) sysconf(_SC_GETPW_R_SIZE_MAX); @@ -878,7 +878,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getpwnam( } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_getpwuid( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_getpwuid( JNIEnv *env, jclass clazz, jint javaUid) { uid_t uid = (uid_t) javaUid; size_t bufferSize = (size_t) sysconf(_SC_GETPW_R_SIZE_MAX); @@ -926,7 +926,7 @@ static char* _hasmntopt(const struct mntent* mnt, const char* opt) { #endif JNIEXPORT jboolean JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_hasmntopt( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_hasmntopt( JNIEnv *env, jclass clazz, jobject javaMntent, jobject javaOption) { struct mntent mntent = {}; mntent.mnt_opts = mallocMntOptsFromStructMntent(env, javaMntent); @@ -943,7 +943,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_hasmntopt( } JNIEXPORT jint JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_inotify_1add_1watch( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_inotify_1add_1watch( JNIEnv *env, jclass clazz, jobject javaFd, jobject javaPath, jint javaMask) { int fd = getFdFromFileDescriptor(env, javaFd); char *path = mallocStringFromByteString(env, javaPath); @@ -958,7 +958,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_inotify_1add_1wat } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_inotify_1init1( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_inotify_1init1( JNIEnv *env, jclass clazz, jint javaFlags) { int flags = javaFlags; int fd = TEMP_FAILURE_RETRY(inotify_init1(flags)); @@ -993,7 +993,7 @@ static jobject newStructInotifyEvent(JNIEnv *env, const struct inotify_event *ev } JNIEXPORT jobjectArray JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_inotify_1get_1events( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_inotify_1get_1events( JNIEnv *env, jclass clazz, jbyteArray javaBuffer, jint javaOffset, jint javaLength) { void *buffer = (*env)->GetByteArrayElements(env, javaBuffer, NULL); size_t offset = (size_t) javaOffset; @@ -1031,7 +1031,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_inotify_1get_1eve } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_inotify_1rm_1watch( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_inotify_1rm_1watch( JNIEnv *env, jclass clazz, jobject javaFd, jint javaWd) { int fd = getFdFromFileDescriptor(env, javaFd); uint32_t wd = (uint32_t) javaWd; @@ -1042,7 +1042,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_inotify_1rm_1watc } JNIEXPORT jint JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_ioctl_1int( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_ioctl_1int( JNIEnv* env, jclass clazz, jobject javaFd, jint javaRequest, jobject javaArgument) { int fd = getFdFromFileDescriptor(env, javaFd); int request = javaRequest; @@ -1064,7 +1064,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_ioctl_1int( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_lchown( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_lchown( JNIEnv *env, jclass clazz, jobject javaPath, jint javaUid, jint javaGid) { char *path = mallocStringFromByteString(env, javaPath); uid_t uid = (uid_t) javaUid; @@ -1077,7 +1077,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_lchown( } JNIEXPORT jbyteArray JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_lgetxattr( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_lgetxattr( JNIEnv *env, jclass clazz, jobject javaPath, jobject javaName) { char *path = mallocStringFromByteString(env, javaPath); char *name = mallocStringFromByteString(env, javaName); @@ -1116,7 +1116,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_lgetxattr( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_link( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_link( JNIEnv *env, jclass clazz, jobject javaOldPath, jobject javaNewPath) { char *oldPath = mallocStringFromByteString(env, javaOldPath); char *newPath = mallocStringFromByteString(env, javaNewPath); @@ -1129,7 +1129,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_link( } JNIEXPORT jobjectArray JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_llistxattr( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_llistxattr( JNIEnv *env, jclass clazz, jobject javaPath) { char *path = mallocStringFromByteString(env, javaPath); jobjectArray javaNames = NULL; @@ -1188,7 +1188,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_llistxattr( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_lsetxattr( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_lsetxattr( JNIEnv *env, jclass clazz, jobject javaPath, jobject javaName, jbyteArray javaValue, jint javaFlags) { char *path = mallocStringFromByteString(env, javaPath); @@ -1263,7 +1263,7 @@ static jobject doStat(JNIEnv *env, jobject javaPath, bool isLstat) { } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_lstat( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_lstat( JNIEnv *env, jclass clazz, jobject javaPath) { return doStat(env, javaPath, true); } @@ -1293,13 +1293,13 @@ doUtimens(JNIEnv *env, jobject javaPath, jobjectArray javaTimes, bool isLutimens } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_lutimens( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_lutimens( JNIEnv *env, jclass clazz, jobject javaPath, jobjectArray javaTimes) { doUtimens(env, javaPath, javaTimes, true); } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_mkdir( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_mkdir( JNIEnv *env, jclass clazz, jobject javaPath, jint javaMode) { char *path = mallocStringFromByteString(env, javaPath); mode_t mode = (mode_t) javaMode; @@ -1311,7 +1311,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_mkdir( } JNIEXPORT jint JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_mount( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_mount( JNIEnv *env, jclass clazz, jobject javaSource, jobject javaTarget, jobject javaFileSystemType, jlong javaMountFlags, jbyteArray javaData) { if (geteuid() != 0) { @@ -1345,7 +1345,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_mount( } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_open( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_open( JNIEnv *env, jclass clazz, jobject javaPath, jint javaFlags, jint javaMode) { char *path = mallocStringFromByteString(env, javaPath); int flags = javaFlags; @@ -1360,7 +1360,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_open( } JNIEXPORT jlong JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_opendir( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_opendir( JNIEnv *env, jclass clazz, jobject javaPath) { char *path = mallocStringFromByteString(env, javaPath); DIR *dir = TEMP_FAILURE_RETRY_N(opendir(path)); @@ -1391,7 +1391,7 @@ static jobject newStructDirent(JNIEnv *env, const struct dirent64 *dirent) { } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_readdir( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_readdir( JNIEnv *env, jclass clazz, jlong javaDir) { DIR *dir = (DIR *) javaDir; struct dirent64 *dirent = TEMP_FAILURE_RETRY_N(readdir64(dir)); @@ -1406,7 +1406,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_readdir( } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_readlink( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_readlink( JNIEnv *env, jclass clazz, jobject javaPath) { char *path = mallocStringFromByteString(env, javaPath); size_t maxSize = PATH_MAX; @@ -1434,7 +1434,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_readlink( } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_realpath( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_realpath( JNIEnv *env, jclass clazz, jobject javaPath) { char *path = mallocStringFromByteString(env, javaPath); char resolvedPath[PATH_MAX] = {}; @@ -1448,7 +1448,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_realpath( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_remove( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_remove( JNIEnv *env, jclass clazz, jobject javaPath) { char *path = mallocStringFromByteString(env, javaPath); int result = TEMP_FAILURE_RETRY(remove(path)); @@ -1461,7 +1461,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_remove( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_rename( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_rename( JNIEnv *env, jclass clazz, jobject javaOldPath, jobject javaNewPath) { char *oldPath = mallocStringFromByteString(env, javaOldPath); char *newPath = mallocStringFromByteString(env, javaNewPath); @@ -1474,7 +1474,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_rename( } JNIEXPORT jlong JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_sendfile( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_sendfile( JNIEnv* env, jclass clazz, jobject javaOutFd, jobject javaInFd, jobject javaOffset, jlong javaCount) { int outFd = getFdFromFileDescriptor(env, javaOutFd); @@ -1498,7 +1498,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_sendfile( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_setgrent(JNIEnv *env, jclass clazz) { +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_setgrent(JNIEnv *env, jclass clazz) { TEMP_FAILURE_RETRY_V(setgrent()); if (errno) { throwSyscallException(env, "setgrent"); @@ -1506,7 +1506,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_setgrent(JNIEnv * } JNIEXPORT jlong JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_setmntent( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_setmntent( JNIEnv *env, jclass clazz, jobject javaPath, jobject javaMode) { char *path = mallocStringFromByteString(env, javaPath); char *mode = mallocStringFromByteString(env, javaMode); @@ -1521,7 +1521,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_setmntent( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_setpwent(JNIEnv *env, jclass clazz) { +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_setpwent(JNIEnv *env, jclass clazz) { TEMP_FAILURE_RETRY_V(setpwent()); if (errno) { throwSyscallException(env, "setpwent"); @@ -1529,7 +1529,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_setpwent(JNIEnv * } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_stat( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_stat( JNIEnv *env, jclass clazz, jobject javaPath) { return doStat(env, javaPath, false); } @@ -1556,7 +1556,7 @@ static jobject newStructStatVfs(JNIEnv *env, const struct statvfs64 *statvfs) { } JNIEXPORT jobject JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_statvfs( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_statvfs( JNIEnv *env, jclass clazz, jobject javaPath) { char *path = mallocStringFromByteString(env, javaPath); struct statvfs64 statvfs = {}; @@ -1570,7 +1570,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_statvfs( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_symlink( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_symlink( JNIEnv *env, jclass clazz, jobject javaTarget, jobject javaLinkPath) { char *target = mallocStringFromByteString(env, javaTarget); char *linkPath = mallocStringFromByteString(env, javaLinkPath); @@ -1583,7 +1583,7 @@ Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_symlink( } JNIEXPORT void JNICALL -Java_me_zhanghai_android_files_provider_linux_syscall_Syscalls_utimens( +Java_me_zhanghai_android_files_provider_linux_syscall_Syscall_utimens( JNIEnv *env, jclass clazz, jobject javaPath, jobjectArray javaTimes) { doUtimens(env, javaPath, javaTimes, false); } From ce1b9edaf42001fecee0cea45b42f7a31a52477e Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 22 Aug 2023 05:24:25 -0700 Subject: [PATCH 102/326] [Feature] Remove STL from libsyscall. --- app/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 5d1d16a6f..385f85f50 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,11 @@ android { buildConfigField 'String', 'FILE_PROVIDIER_AUTHORITY', 'APPLICATION_ID + ".file_provider"' resValue 'string', 'app_provider_authority', applicationId + '.app_provider' resValue 'string', 'file_provider_authority', applicationId + '.file_provider' + externalNativeBuild { + cmake { + arguments "-DANDROID_STL=none" + } + } } buildFeatures { aidl true From e451d96203550fcbeb877fd321a7ca15c5c15682 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 22 Aug 2023 14:39:30 -0700 Subject: [PATCH 103/326] [Feature] Update AGP. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f7b470afe..d10b98f89 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From 0540ce37ddca687a3b6ebdf88b1b3aaf19d3ec2b Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 22 Aug 2023 14:44:11 -0700 Subject: [PATCH 104/326] [Feature] Update Kotlin to 1.9.0. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d10b98f89..ab58a91df 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext { - kotlin_version = '1.8.22' + kotlin_version = '1.9.0' } repositories { google() From fef528f97271ada0aa2ccb0268136b95881c4a45 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 22 Aug 2023 14:49:45 -0700 Subject: [PATCH 105/326] [Refactor] Use Kotlin 1.9.0 stable enum entries instead of values(). --- .../main/java/me/zhanghai/android/files/app/AppUpgraders.kt | 4 ++-- .../me/zhanghai/android/files/filelist/FileListAdapter.kt | 2 +- .../android/files/navigation/NavigationListAdapter.kt | 4 ++-- .../android/files/provider/ftp/FtpFileSystemProvider.kt | 2 +- .../zhanghai/android/files/storage/EditFtpServerFragment.kt | 6 +++--- .../android/files/storage/EditSftpServerFragment.kt | 2 +- .../zhanghai/android/files/storage/EditSmbServerFragment.kt | 2 +- .../android/files/theme/custom/ThemeColorPreference.kt | 3 +-- 8 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/app/AppUpgraders.kt b/app/src/main/java/me/zhanghai/android/files/app/AppUpgraders.kt index 60caef05f..88c76dbbe 100644 --- a/app/src/main/java/me/zhanghai/android/files/app/AppUpgraders.kt +++ b/app/src/main/java/me/zhanghai/android/files/app/AppUpgraders.kt @@ -94,8 +94,8 @@ private fun migrateFileSortOptionsSetting1_1_0(sharedPreferences: SharedPreferen oldParcel.unmarshall(oldBytes, 0, oldBytes.size) oldParcel.setDataPosition(0) newParcel.writeString(oldParcel.readString()) - newParcel.writeString(FileSortOptions.By.values()[oldParcel.readInt()].name) - newParcel.writeString(FileSortOptions.Order.values()[oldParcel.readInt()].name) + newParcel.writeString(FileSortOptions.By.entries[oldParcel.readInt()].name) + newParcel.writeString(FileSortOptions.Order.entries[oldParcel.readInt()].name) newParcel.writeInt(oldParcel.readByte().toInt()) } newParcel.marshall() diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt index 033f32248..fdf2e8bc1 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt @@ -170,7 +170,7 @@ class FileListAdapter( override fun getItemViewType(position: Int): Int = viewType.ordinal override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val viewType = FileViewType.values()[viewType] + val viewType = FileViewType.entries[viewType] val inflater = parent.context.layoutInflater val holder = when (viewType) { FileViewType.LIST -> ViewHolder(FileItemListBinding.inflate(inflater, parent, false)) diff --git a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationListAdapter.kt index 824d02d2a..b6d45b3a2 100644 --- a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationListAdapter.kt +++ b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationListAdapter.kt @@ -160,7 +160,7 @@ class NavigationListAdapter( } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (ViewType.values()[viewType]) { + return when (ViewType.entries[viewType]) { ViewType.ITEM -> ItemHolder( NavigationItemBinding.inflate(parent.context.layoutInflater, parent, false) @@ -222,7 +222,7 @@ class NavigationListAdapter( position: Int, payloads: List ) { - when (ViewType.values()[getItemViewType(position)]) { + when (ViewType.entries[getItemViewType(position)]) { ViewType.ITEM -> { val item = getItem(position)!! val binding = (holder as ItemHolder).binding diff --git a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystemProvider.kt b/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystemProvider.kt index 67d27f467..893a43403 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystemProvider.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/ftp/FtpFileSystemProvider.kt @@ -113,7 +113,7 @@ object FtpFileSystemProvider : FileSystemProvider(), PathObservableProvider, Sea val username = userInfo ?: "" val queryUri = decodedQueryByteString?.toString()?.let { Uri.parse(it) } val mode = queryUri?.getQueryParameter(FtpPath.QUERY_PARAMETER_MODE) - ?.let { mode -> Mode.values().first { it.name.equals(mode, true) } } + ?.let { mode -> Mode.entries.first { it.name.equals(mode, true) } } ?: Authority.DEFAULT_MODE val encoding = queryUri?.getQueryParameter(FtpPath.QUERY_PARAMETER_ENCODING) ?: Authority.DEFAULT_ENCODING diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt index bde9348c0..9a99887e2 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditFtpServerFragment.kt @@ -207,7 +207,7 @@ class EditFtpServerFragment : Fragment() { val items = List(adapter.count) { adapter.getItem(it) as CharSequence } val selectedItem = binding.protocolEdit.text val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return Protocol.values()[selectedIndex] + return Protocol.entries[selectedIndex] } set(value) { val adapter = binding.protocolEdit.adapter @@ -221,7 +221,7 @@ class EditFtpServerFragment : Fragment() { val items = List(adapter.count) { adapter.getItem(it) as CharSequence } val selectedItem = binding.authenticationTypeEdit.text val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return AuthenticationType.values()[selectedIndex] + return AuthenticationType.entries[selectedIndex] } set(value) { val adapter = binding.authenticationTypeEdit.adapter @@ -241,7 +241,7 @@ class EditFtpServerFragment : Fragment() { val items = List(adapter.count) { adapter.getItem(it) as CharSequence } val selectedItem = binding.modeEdit.text val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return Mode.values()[selectedIndex] + return Mode.entries[selectedIndex] } set(value) { val adapter = binding.modeEdit.adapter diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditSftpServerFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditSftpServerFragment.kt index 42ec61b4e..ad09b15f9 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditSftpServerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditSftpServerFragment.kt @@ -186,7 +186,7 @@ class EditSftpServerFragment : Fragment() { val items = List(adapter.count) { adapter.getItem(it) as CharSequence } val selectedItem = binding.authenticationTypeEdit.text val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return AuthenticationType.values()[selectedIndex] + return AuthenticationType.entries[selectedIndex] } set(value) { val adapter = binding.authenticationTypeEdit.adapter diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditSmbServerFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditSmbServerFragment.kt index eeea3fa20..d6a71864f 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditSmbServerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditSmbServerFragment.kt @@ -197,7 +197,7 @@ class EditSmbServerFragment : Fragment() { val items = List(adapter.count) { adapter.getItem(it) as CharSequence } val selectedItem = binding.authenticationTypeEdit.text val selectedIndex = items.indexOfFirst { TextUtils.equals(it, selectedItem) } - return AuthenticationType.values()[selectedIndex] + return AuthenticationType.entries[selectedIndex] } set(value) { val adapter = binding.authenticationTypeEdit.adapter diff --git a/app/src/main/java/me/zhanghai/android/files/theme/custom/ThemeColorPreference.kt b/app/src/main/java/me/zhanghai/android/files/theme/custom/ThemeColorPreference.kt index 973437030..07db243a5 100644 --- a/app/src/main/java/me/zhanghai/android/files/theme/custom/ThemeColorPreference.kt +++ b/app/src/main/java/me/zhanghai/android/files/theme/custom/ThemeColorPreference.kt @@ -70,8 +70,7 @@ class ThemeColorPreference : BaseColorPreference { init { val context = context - entryValues = ThemeColor.values().map { context.getColorCompat(it.resourceId) } - .toIntArray() + entryValues = ThemeColor.entries.map { context.getColorCompat(it.resourceId) }.toIntArray() } override fun onGetDefaultValue(a: TypedArray, index: Int): Any? = From d8be8b7c6aca5a67dee3ff74f28b962bd57d25e8 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 22 Aug 2023 14:59:16 -0700 Subject: [PATCH 106/326] [Refactor] Use Kotlin 1.9.0 ..< instead of until. --- .../me/zhanghai/android/files/compat/NioUtilsCompat.kt | 2 +- .../java/me/zhanghai/android/files/compat/ParcelCompat.kt | 2 +- .../main/java/me/zhanghai/android/files/file/MimeType.kt | 8 ++++---- .../zhanghai/android/files/filelist/BreadcrumbLayout.kt | 4 ++-- .../android/files/filelist/CollatorFileNameExtensions.kt | 2 +- .../me/zhanghai/android/files/filelist/FileListAdapter.kt | 4 ++-- .../zhanghai/android/files/filelist/FileListFragment.kt | 2 +- .../java/me/zhanghai/android/files/filelist/TrailData.kt | 2 +- .../zhanghai/android/files/provider/common/ByteString.kt | 6 +++--- .../android/files/provider/common/ByteStringBuilder.kt | 2 +- .../me/zhanghai/android/files/ui/AnimatedListAdapter.kt | 2 +- .../android/files/ui/CoordinatorScrollingFrameLayout.kt | 2 +- .../android/files/ui/CoordinatorScrollingLinearLayout.kt | 2 +- .../java/me/zhanghai/android/files/ui/SimpleAdapter.kt | 2 +- .../me/zhanghai/android/files/util/AbstractLocalCursor.kt | 4 ++-- 15 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/compat/NioUtilsCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/NioUtilsCompat.kt index a1e8c113d..9f4880269 100644 --- a/app/src/main/java/me/zhanghai/android/files/compat/NioUtilsCompat.kt +++ b/app/src/main/java/me/zhanghai/android/files/compat/NioUtilsCompat.kt @@ -26,7 +26,7 @@ object NioUtilsCompat { ) fun newFileChannel(ioObject: Closeable, fd: FileDescriptor, flags: Int): FileChannel = - if (Build.VERSION.SDK_INT in Build.VERSION_CODES.N until Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT in Build.VERSION_CODES.N..> Parcel.readParcelableListCompat( return list } val listSize = list.size - for (index in 0 until size) { + for (index in 0..(classLoader) as E if (index < listSize) { diff --git a/app/src/main/java/me/zhanghai/android/files/file/MimeType.kt b/app/src/main/java/me/zhanghai/android/files/file/MimeType.kt index f74b13264..ade3411c9 100644 --- a/app/src/main/java/me/zhanghai/android/files/file/MimeType.kt +++ b/app/src/main/java/me/zhanghai/android/files/file/MimeType.kt @@ -76,19 +76,19 @@ fun String.asMimeType(): MimeType { private val String.isValidMimeType: Boolean get() { val indexOfSlash = indexOf('/') - if (indexOfSlash == -1 || indexOfSlash !in 1 until length) { + if (indexOfSlash == -1 || indexOfSlash !in 1.. indexOfSemicolon)) { - if (indexOfPlus !in indexOfSlash + 2 - until if (indexOfSemicolon != -1) indexOfSemicolon - 1 else length) { + if (indexOfPlus !in indexOfSlash + 2..< + if (indexOfSemicolon != -1) indexOfSemicolon - 1 else length) { return false } } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/BreadcrumbLayout.kt b/app/src/main/java/me/zhanghai/android/files/filelist/BreadcrumbLayout.kt index 33db95df7..34bbd6c9e 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/BreadcrumbLayout.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/BreadcrumbLayout.kt @@ -154,10 +154,10 @@ class BreadcrumbLayout : HorizontalScrollView { private fun inflateItemViews() { // HACK: Remove/add views at the front so that ripple remains correct, as we are potentially // collapsing/expanding breadcrumbs at the front. - for (index in data.paths.size until itemsLayout.childCount) { + for (index in data.paths.size..() // We need the ordered list from our adapter instead of the list from FileListLiveData. - for (index in 0 until adapter.itemCount) { + for (index in 0..( private fun clearAnimation() { stopAnimation() recyclerView?.let { - for (index in 0 until it.childCount) { + for (index in 0.. : RecyclerView.Ada fun findPositionById(id: Long): Int { val count = itemCount - for (index in 0 until count) { + for (index in 0.. Date: Tue, 22 Aug 2023 22:26:59 -0700 Subject: [PATCH 107/326] [Feature] Add -Wall -Werror for native code. --- app/CMakeLists.txt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 815c17e26..608a96d11 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -2,8 +2,20 @@ cmake_minimum_required(VERSION 3.6) project(MaterialFiles C) -add_library(hiddenapi SHARED src/main/jni/hiddenapi.c) +add_library(hiddenapi SHARED + src/main/jni/hiddenapi.c) +target_compile_options(hiddenapi + PRIVATE + -Wall + -Werror) find_library(LOG_LIBRARY log) -add_library(syscall SHARED src/main/jni/syscall.c) -target_link_libraries(syscall ${LOG_LIBRARY}) +add_library(syscall SHARED + src/main/jni/syscall.c) +target_compile_options(syscall + PRIVATE + -Wall + -Werror) +target_link_libraries(syscall + PRIVATE + ${LOG_LIBRARY}) From 9265614f3493c6030d09b04ec784f7ba63ab3e8b Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 23 Aug 2023 00:02:29 -0700 Subject: [PATCH 108/326] [Refactor] Fix formatting. --- .../zhanghai/android/files/provider/linux/syscall/Syscall.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscall.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscall.kt index 285480a4f..a1d3bdefa 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscall.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/syscall/Syscall.kt @@ -268,8 +268,8 @@ object Syscall { external fun sendfile( outFd: FileDescriptor, inFd: FileDescriptor, - offset: Int64Ref? - , count: Long + offset: Int64Ref?, + count: Long ): Long @Throws(SyscallException::class) From 5796a9ed4b09034fca651ec2fcfa25956318ee44 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 25 Aug 2023 13:15:10 -0700 Subject: [PATCH 109/326] [Feature] Update Kotlin to 1.9.10. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ab58a91df..02730f286 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { ext { - kotlin_version = '1.9.0' + kotlin_version = '1.9.10' } repositories { google() From e249ced2c0ae29e5263bd0f7bb39fe02ef330f29 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 27 Aug 2023 16:34:51 -0700 Subject: [PATCH 110/326] [Feature] Add new languages. --- fastlane/metadata/android/en-US/changelogs/32.txt | 1 + fastlane/metadata/android/zh-CN/changelogs/32.txt | 1 + utils/import-translations.sh | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/fastlane/metadata/android/en-US/changelogs/32.txt b/fastlane/metadata/android/en-US/changelogs/32.txt index 0f4b517f2..0f3a7a6a7 100644 --- a/fastlane/metadata/android/en-US/changelogs/32.txt +++ b/fastlane/metadata/android/en-US/changelogs/32.txt @@ -3,6 +3,7 @@ - Added shortcut to DocumentsUI for accessing Android/data and Android/obb. - Added URL information to FTP server notification. - Added banner for Android TV. +- Added Greek, Finnish, Lithuanian, Norwegian Bokmål and Ukrainian translation. - Fast scroll popup now shows text according to the current sort options. - Video thumbnail is now taken from 1/3 of the video instead of the first frame. - Material Design 2 theme will be removed in the upcoming version 1.7.0 to ease code maintenance. diff --git a/fastlane/metadata/android/zh-CN/changelogs/32.txt b/fastlane/metadata/android/zh-CN/changelogs/32.txt index 796540a47..dcfbac644 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/32.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/32.txt @@ -3,6 +3,7 @@ - 添加了指向 DocumentsUI 的快捷方式以访问 Android/data 和 Android/obb。 - 向 FTP 服务器的通知添加了 URL 信息。 - 为 Android TV 添加了横幅。 +- 添加了希腊语、芬兰语、立陶宛语、书面挪威语和乌克兰语翻译。 - 快速滚动的提示现在会根据排序选项来显示文字。 - 视频缩略图现在会从视频的 1/3 处而非第一帧获取。 - Material Design 2 主题将在未来的 1.7.0 版本中被移除以便于代码维护。 diff --git a/utils/import-translations.sh b/utils/import-translations.sh index 171ee9fb2..ca0141033 100755 --- a/utils/import-translations.sh +++ b/utils/import-translations.sh @@ -6,9 +6,11 @@ LOCALES=( bg cs de + el es eu fa + fi fr hu in @@ -17,6 +19,8 @@ LOCALES=( iw ja ko + lt + nb nl pl pt-rBR @@ -24,6 +28,7 @@ LOCALES=( ro ru tr + uk vi ) From dc5f5518dbd9e75e354b0b220d0f35da85a97e93 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 27 Aug 2023 17:51:22 -0700 Subject: [PATCH 111/326] [Feature] Import translations. --- app/src/main/res/values-ar/strings.xml | 18 +- app/src/main/res/values-bg/mime_types.xml | 14 +- app/src/main/res/values-bg/strings.xml | 354 +++++---- app/src/main/res/values-cs/strings.xml | 4 - app/src/main/res/values-de/strings.xml | 18 +- app/src/main/res/values-el/mime_types.xml | 39 + app/src/main/res/values-el/strings.xml | 613 ++++++++++++++++ app/src/main/res/values-es/strings.xml | 36 +- app/src/main/res/values-eu/strings.xml | 4 - app/src/main/res/values-fa/strings.xml | 59 +- app/src/main/res/values-fi/mime_types.xml | 31 + app/src/main/res/values-fi/strings.xml | 607 +++++++++++++++ app/src/main/res/values-fr/strings.xml | 65 +- app/src/main/res/values-hu/strings.xml | 82 ++- app/src/main/res/values-in/mime_types.xml | 7 +- app/src/main/res/values-in/strings.xml | 52 +- app/src/main/res/values-is/strings.xml | 18 +- app/src/main/res/values-it/strings.xml | 57 +- app/src/main/res/values-iw/strings.xml | 4 - app/src/main/res/values-ja/strings.xml | 4 +- app/src/main/res/values-ko/strings.xml | 20 +- app/src/main/res/values-lt/mime_types.xml | 39 + app/src/main/res/values-lt/strings.xml | 693 ++++++++++++++++++ app/src/main/res/values-nb/mime_types.xml | 39 + app/src/main/res/values-nb/strings.xml | 629 ++++++++++++++++ app/src/main/res/values-nl/strings.xml | 18 +- app/src/main/res/values-pl/strings.xml | 28 +- app/src/main/res/values-pt-rBR/mime_types.xml | 4 +- app/src/main/res/values-pt-rBR/strings.xml | 115 ++- app/src/main/res/values-pt-rPT/strings.xml | 137 +++- app/src/main/res/values-ro/strings.xml | 18 +- app/src/main/res/values-ru/strings.xml | 12 +- app/src/main/res/values-tr/strings.xml | 4 - app/src/main/res/values-uk/mime_types.xml | 39 + app/src/main/res/values-uk/strings.xml | 677 +++++++++++++++++ app/src/main/res/values-vi/strings.xml | 4 - 36 files changed, 4279 insertions(+), 283 deletions(-) create mode 100644 app/src/main/res/values-el/mime_types.xml create mode 100644 app/src/main/res/values-el/strings.xml create mode 100644 app/src/main/res/values-fi/mime_types.xml create mode 100644 app/src/main/res/values-fi/strings.xml create mode 100644 app/src/main/res/values-lt/mime_types.xml create mode 100644 app/src/main/res/values-lt/strings.xml create mode 100644 app/src/main/res/values-nb/mime_types.xml create mode 100644 app/src/main/res/values-nb/strings.xml create mode 100644 app/src/main/res/values-uk/mime_types.xml create mode 100644 app/src/main/res/values-uk/strings.xml diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 600cff279..9465e991b 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -43,6 +43,7 @@ إظهار تخطى إيقاف + النظام الافتراضي غير معروف عرض @@ -383,6 +384,9 @@ ,\u0020 لا يمكن أن يكون المسار فارغًا مسار غير صالح + عرض وفرز + قائمة + شبكة اسم نوع حجم @@ -399,7 +403,7 @@ اضافة للإشاراة المرجعية إنشاء اختصار افتح في نافذة جديدة - تم تحديد %1$,d + %1$,d تحديد ”%1$s“ نقل %1$,d نسخ %1$,d @@ -541,6 +545,10 @@ تحرير وحدة تخزين الجهاز الاسم المسار + إضافة اختصار واجهة المستخدم للوثائق + تعديل اختصار واجهة المستخدم للوثائق + أدخل URI + URI غير صالح أضف وحدة تخزين خارجية تحرير وحدة التخزين الخارجية الاسم @@ -664,9 +672,13 @@ جاري الإيقاف… لم يبدأ الرابط - لا يمكن استرداد عنوان IP المحلي + عنوان IP المحلي غير معروف نسخ الرابط نسخ كلمة المرور + أضف إلى الإعدادات السريعة + تمت إضافة \"خادم FTP\" إلى الإعدادات السريعة + تمت إضافة \"خادم FTP\" إلى الإعدادات السريعة بالفعل + خطأ أثناء إضافة \"خادم FTP\" إلى الإعدادات السريعة التهيئة تسجيل دخول مجهول اسم المستخدم @@ -674,9 +686,9 @@ منفذ المجلد الرئيسي اسمح بالكتابة - الإعدادات واجهة المستخدم + لغة لون الثيم اللون الذي يظهر بشكل متكرر في التطبيق تصميم متيريال 3 diff --git a/app/src/main/res/values-bg/mime_types.xml b/app/src/main/res/values-bg/mime_types.xml index bce5e3dbc..f633a99c1 100644 --- a/app/src/main/res/values-bg/mime_types.xml +++ b/app/src/main/res/values-bg/mime_types.xml @@ -16,24 +16,24 @@ Електронна визитка Папка %1$s документ - %1$s е-книга + %1$s електронна книга %1$s електронно съобщение %1$s шрифт %1$s файл %1$s изображение - PDF документ + Документ на PDF %1$s презентация %1$s таблица %1$s документ Текстов документ %1$s видео - Word документ - PowerPoint презентация - Excel таблица + Документ на Word + Презентация на PowerPoint + Електронна таблица на Excel Символно устройство Блоково устройство Тръба (Pipe) - Връзка + Препратка Socket - Връзка (счупена) + Препратка (неработеща) diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index b2df426e7..307431246 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -15,6 +15,7 @@ По подразбиране Изтриване Отказ + Редактиране Празно (Празно) Грешка @@ -22,9 +23,10 @@ Инсталиране Продължаване редактирането Зареждане… - По къно + По-късно Обединаване Няма + Отваря менюто Поставяне Опресняване Презареждане @@ -33,7 +35,7 @@ Замяна Нулиране Повтаряне - Запис + Запазване Търсене Избиране Избиране всички @@ -41,12 +43,13 @@ Показване Пропускане Спиране + Според системата Неизвестно Изглед - Няма намерено приложение за това действие - Отваряне на настройки + Не е намерено приложение, което да извърши действието + Настройки Прекият път е създаден @@ -54,121 +57,123 @@ %1$,d байта - Трябват права за достъпа до файлове. Моля натиснете “Разрешаване” в следващия системен прозорец - Трябват права за достъпа до файлове. Моля разрешете “ХРАНИЛИЩЕ” в правата в системните настройки. + Необходими са права за достъп до файлове. Докоснете „Разрешаване“ в следващия системен прозорец + Необходими са права за достъпа до файлове. Разрешете правото „ХРАНИЛИЩЕ“ в системните настройки. + Необходими са права за управление на всички файлове. Дайте достъп в последващите системни настройки. + Действия на заден план Изпълняване на действия докато приложението е на заден план - Файлови операции - Показване и контролиране на файлови операции - FTP сървър - Показване и контролиране на FTP сървъра + Действия с файлове + Показване и управление на действия с файлове + Сървър на FTP + Показване и управление на сървър на FTP - Приготвяне да се компресира %1$,d файл (%2$s) - Подготвяне да се компресират %1$,d файла (%2$s) + Подготвя се за компресиране %1$,d файл (%2$s) + Подготвят се за компресиране %1$,d файла (%2$s) - Компресиране на “%1$s” в “%2$s” + Компресиране на „%1$s“ в „%2$s“ - Компресиране на %1$,d файл в “%2$s” - Компресиране на %1$,d файла в “%2$s” + Компресиране на %1$,d файл в „%2$s“ + Компресиране на %1$,d файла в „%2$s“ - Приготвяне да се копира %1$,d файл (%2$s) - Подготвяне да се копират %1$,d файла (%2$s) + Подготвя се за копиране %1$,d файл (%2$s) + Подготвят се за копиране %1$,d файла (%2$s) Копиране на %1$s в %2$s - Копиране на %1$,d файл в “%2$s” - Копиране на %1$,d файла в “%2$s” + Копиране на %1$,d файл в „%2$s“ + Копиране на %1$,d файла в „%2$s“ - Приготвяне да се извлече %1$,d файл (%2$s) - Подготвяне да се извлекат %1$,d файла (%2$s) + Приготвя се за извличане %1$,d файл (%2$s) + Подготвят се за извличане %1$,d файла (%2$s) - Извличане на “%1$s” в “%2$s” + Извличане на „%1$s“ в „%2$s“ Извличане на %1$,d файл в “%2$s” - Извличане на %1$,d файла в “%2$s” + Извличане на %1$,d файла в „%2$s“ - Подготвяне да се премести %1$,d файл (%2$s) - Подготвяне да се преместят %1$,d файла (%2$s) + Подготвя се за преместване %1$,d файл (%2$s) + Подготвят се за преместване %1$,d файла (%2$s) - Преместване на “%1$s” в “%2$s” + Преместване на „%1$s“ в „%2$s“ - Преместване на %1$,d файл към “%2$s” - Преместване на %1$,d файла към “%2$s” + Преместване на %1$,d файл към „%2$s“ + Преместване на %1$,d файла към „%2$s“ %1$s / %2$s %1$,d / %2$,d - Подготвяне да се изтрие %1$,d файл - Подготвяне да се изтрият %1$,d файла + Подготвя се за изтриване %1$,d файл + Подготвят се за изтриване %1$,d файла - Изтриване на “%1$s” + Изтриване на „%1$s“ Изтриване на %1$,d файл Изтриване на %1$,d файла - Подготвяне за смяна на притежателя на %1$,d файл - Подготвяне за смяна на притежателя на %1$,d файла + Подготвя се за смяна на притежателя %1$,d файл + Подготвят се за смяна на притежателя %1$,d файла - Смяна на притежателя на “%1$s” + Промяна на притежателя на „%1$s“ - Смяна на притежателя на %1$,d файл - Смяна на притежателя на %1$,d файла + Промяна на притежателя на %1$,d файл + Промяна на притежателя на %1$,d файла - Подготовка за промяна на групата на %1$,d файл - Подготвяне за промяна на групата на %1$,d файла + Подготвя се за промяна на групата %1$,d файл + Подготвят се за промяна на групата %1$,d файла - Промяна на групата на “%1$s” + Промяна на групата на „%1$s“ - Смяна на групата на %1$,d файл - Смяна на групата на %1$,d файла + Промяна на групата на %1$,d файл + Промяна на групата на %1$,d файла - Подготовка за смяна на правата на %1$,d файл - Подготвяне за смяна на правата на %1$,d файла + Подготвят се за промяна на правата %1$,d файл + Подготвят се за промяна на правата %1$,d файла - Смяна на правата на “%1$s” + Промяна на правата на „%1$s“ - Смяна на правата на %1$,d файл - Смяна на правата на %1$,d файла + Промяна на правата на %1$,d файл + Промяна на правата на %1$,d файла - Подготовка за смяна на SELinux контекста на %1$,d файл - Подготвяне за смяна на SELinux контекста на %1$,d файла + Подготвя се за смяна на контекста на SELinux %1$,d файл + Подготвят се за смяна на контекста на SELinux %1$,d файла - Промяна на SELinux контекста за “%1$s” + Промяна на контекста на SELinux на „%1$s“ Промяна на SELinux контекста на %1$,d файл - Промяна на SELinux контекста на %1$,d файла + Промяна на контекста на SELinux на %1$,d файла - Подготовка за възстановяване на SELinux контекста на %1$,d файл - Подготвяне за възстановяване на SELinux контекста на %1$,d файла + Подготвя се за възстановяване на контекста на SELinux %1$,d файл + Подготвят се за възстановяване на контекста на SELinux %1$,d файла - Възстановяване на SELinux контекста за “%1$s” + Възстановяване на контекста на SELinux „%1$s“ Възстановяване на SELinux контекста за %1$,d файл - Възстановяване на SELinux контекста за %1$,d файлове + Възстановяване на контекста на SELinux %1$,d файла %1$,d / %2$,d - Записване на “%1$s” - Грешка при създаването - Възникна грешка при създаването на “%1$s”.\n%2$s - Не може да се копира папка в себе си - Не може да се извлече папка в себе си - Не може да се премести папка в себе си + Записване на „%1$s“ + Грешка при създаване + Грешка при създаване на „%1$s“.\n%2$s + Папката не може да бъде копирана в себе си + Папката не може да бъде извлечена в себе си + Папката не може да бъде преместена в себе си Целевата папка е в папката източник - Не може да се копира файл върху себе си - Не може да се извлече файл върху себе си - Не може да се премести файл върху себе си - Не може да се копира/мести върху себе си - Замяна на файла “%1$s”? - Друг файл със същото име вече съществува в “%1$s”.\nПодмяната му ще презапише съдържанието му. + Файлът не може да бъде копиран върху себе си + Файлът не може да бъде извлечен върху себе си + Файлът не може да бъде преместен върху себе си + Изходният файл би бил презаписан от целевия. + Заменяне на файла „%1$s“? + Друг файл със същото име вече съществува в “%1$s”.\nПри презаписване ще бъде заменено и съдържанието му. Оригинален файл Замяна с Обединяване на папката “%1$s”? @@ -213,20 +218,32 @@ Отваряне като… Отваряне на “%1$s” като Текст - Каркинка + Изображение Аудио Видео Папка Друго - “%1$s” е готово да бъде инсталирано - Натиснете за да инсталирате - “%1$s” е готово да бъде отворено - Натиснете за да отворите - Файлът на може да е празен + Приложението „%1$s” е готово да бъде инсталирано + Натиснете за инсталиране + Файлът „%1$s“ е готов да бъде отворен + Натиснете за отворяне + Файлът не може да е празен Невалидно име на файл Файл с това име вече съществува Изтриване на “%1$s”? - Изтриване на папката “%1$s” и нейното съдържание? + Изтриване на папката „%1$s“ и нейното съдържание? + + Премахване на %1$,d файл? + Премахване на %1$,d файла? + + + Премахване на %1$,d папка и съдържанието ѝ? + Премахване на %1$,d папки и съдържанието им? + + + Премахване на %1$,d елемент? + Премахване на %1$,d елемента? + Създаване на архив .zip .tar.xz @@ -253,23 +270,29 @@ %1$,d файлове ,\u0020 + Пътят не може да е празен + Неприемлив път + Преглед и сортиране + Списък + Решетка Име - Тип + Вид Размер Последна промяна Възходящ Първо папките - Само тази папка + Само за тази папка Нов прозорец Нагоре + Към Показване на скрити файлове Копиране на път Отваряне в терминал Добавяне на отметка Създаване на пряк път Отваряне в нов прозорез - %1$,d избран(и) - Избиране на “%1$s” + %1$,d + Избиране на „%1$s” Преместване на %1$,d Копиране на %1$,d Извличане на %1$,d @@ -289,8 +312,8 @@ Име Тип %1$s (%2$s) - Връзка към %1$s(%2$s) - Цел на връзката + Препратка към %1$s(%2$s) + Цел на препратката Размер %1$s (%2$s) Съдържание @@ -318,7 +341,7 @@ Четене Писане - Стартиране + Изпълняване Четене @@ -327,13 +350,13 @@ Специални - Set UID - Set GID - Sticky бит + Задаване на UID + Задаване на GID + Лепкав бит - Да не се добавя “Изпълнение” рекурсивно - SELinux контекст - Смяна на SELinux контекст + Да не се добавя „Изпълняване“ рекурсивно + Контекст на SELinux + Смяна на контекста на SELinux Възстановяване Рекурсивно прилагане Изображение @@ -344,14 +367,14 @@ %1$.3f, %2$.3f Местоположене Височина - %1$,.3f m + %1$,.3f м Камера %1$s %2$s - Апертура - Shutter speed + Бленда + Скорост на затвора Фокусно разстояние - %1$.2f mm - ISO еквивалент + %1$.2f мм + Еквивалент по ISO ISO %1$d Софтуер Описание @@ -364,19 +387,19 @@ Артист на албума Композитор Диск - Track + Песен Година Жанр Времетраене - Bit rate - %1$d kbps - Sample rate - %1$d Hz + Скорост на данните + %1$d Кб/с + Честота на дискретизация + %1$d Хц Видео APK Име Име на пакета - Версия + Издание %1$s (%2$d) Минимална версия на системата Целева версия на системата @@ -385,33 +408,99 @@ Няма заявени права Заявен %1$,d право - Заявени %1$,d права + Поискани са %1$,d права Подписи + Няма валидни подписи Стари подписи - Начална + Коренова папка Хранилище Няма хранилища Добавяне на хранилище Външно хранилище - SMB сървър - Редактиране на хранилището на устройството + Сървър за FTP + Сървър за SFTP + Сървър за SMB + Редактиране на хранилище Име Път + Въведете адрес + Неприемлив адрес Добавяне на външно хранилище Редакция на външно хранилище Име URI Път + Променяне на сървър за FTP + Нов сървър за FTP + Име на машина + Въведете име на машина + Недействително име на машина + Порт + Неприемлив порт + Път + Може да бъде празно + Наименование + Използване на името на машина + Протокол + Удостоверяване + + Парола + Анонимно + + Потребител + Въведете потребителско име + Парола + Режим + + Активен + Пасивен + + Кодиране на знаците + Свързване и добавяне + Добавяне + Променяне на сървър за SFTP + Нов сървър за SFTP + Име на машина + Въведете име на машина + Недействително име на машина + Порт + Неприемлив порт + Път + Може да бъде празно + Име + Използване на името на машина + Удостоверяване + + Парола + Явен ключ + + Потребител + Въведете потребителско име + Парола + Скрит ключ + Отваряне на файл + Въведете скрит ключ + Неприемлив скрит ключ + Парола на скрития ключ + Може да бъде празно + Неприемлива парола за скрит ключ + Свързване и добавяне + Добавяне Търсене на SMB сървъри… Ръчно добавяне Редакция на SMB сървър Добавяне на SMB сървър Име на машина + Въведете име на машина + Недействително име на машина Порт + Неприемлив порт + Път + Може да бъде празно Име - Използване на името на хоста + Използване на името на машина Тип удостоверяване Парола @@ -419,63 +508,68 @@ Анонимен Потребител + Въведете потребителско име Парола Домейн Свързване и добавяне Добавяне - %1$s свободни от %2$s + Свободни са %1$s от %2$s Добавяне на хранилище… - Аларми + Будилници DCIM Документи Изтегляния Филми Музика Известия - Картинтки + Картини Podcasts Тонове за звънене QQ TIM WeChat - Отметната папка + Отмятане на папка Име Път + Преглед на архиви Преглед на изображение %1$,d/%2$,d Текстов редактор %1$s *%1$s Запазен - Сигурни ли сте, че искате да презаредите? Незапазените промени по този документ ще се загубят. - Сигурни ли сте, че искате да отмените незаписаните промени в този документ? + Сигурни ли сте, че желаете да презаредите? Незапазените промени в документа ще бъдат загубени. + Кодиране на знаците + Сигурни ли сте, че желаете да отхвърлите незапазените промени в документа? - FTP сървър - Статус + Сървър за FTP + Състояние Стартиране… Работещ Спиране… Не е стартиран URL - Не може да се вземе локалния IP адрес + Местният адрес по IP е неизвестен Копиране на URL Копиране на парола - Конфигурация + Добавяне към бързите настройки + Настройки Анонимно вписване Потребител Парола Порт - Основна папка + Коренова папка Разрешаване на писане - Настройки Интерфейс + Език Цвят на темата Цвят, който се ползва най-често в приложението + Материален дизайн 3 Нощен режим - Използвай системния + Спрямо системата Изк. Вкл. Според времето @@ -485,10 +579,10 @@ Анимиране на списъка на файлове Показване на дълги файлови имена - Скриване на началото - Скриване на средата - Скриване на края - Marquee + Многоточие в началото + Многоточие в средата + Многоточие в края + Плъзгане Поведение Папка по подразбиране @@ -497,32 +591,34 @@ Екранни снимки Отметнати папки Няма отметнати папки - Root достъп + Достъп от суперпотребителя - Имате изпълняваща се %1$,d файлова операция и промяната на root режима за достъп може да доведе до неочаквана грешка. Сигурни ли сте, че искате да го смените сега? - Имате изпълняващи се %1$,d файлови операции и промяната на root режима за достъп може да доведе до неочаквана грешка. Сигурни ли сте, че искате да го смените сега? + В момента се извършва действие с %1$,d файл и промяната на режима за достъп до правата на суперпотребителя може да доведе до неочаквана грешка. Сигурни ли сте, че желаете да промените режима сега? + В момента се извършват действия с %1$,d файла и промяната на режима за достъп до правата на суперпотребителя може да доведе до неочаквани грешки. Сигурни ли сте, че желаете да промените режима сега? Само нормален достъп - Automatic - Само root достъп + Автоматичен + Само достъп от суперпотребител - Кодиране на името + Кодиране на имената на архиви Отваряне на пакет на Андроид Инсталиране Преглед на съдържанието - Питане какво да прави + Питане за действие + Миниатюри за файловете на PDF + Има опасност да направи приложението нестабилно под по-ранни издания на Андроид Четене на отдалечени файлове за миниатюри Относно Версия - Прегледай в GitHub + Преглед в GitHub Лицензи Политика за поверителност Автор Hai Zhang - Следене в GitHub - Следене в Twitter + Следване в GitHub + Следване в Twitter diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 86cfb7726..84f654726 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -312,7 +312,6 @@ Naposledy změněno Vzestupně Složky jako první - Pouze pro tuto složku Nové okno Nahoru Zobrazit skryté soubory @@ -321,7 +320,6 @@ Přidat záložku Vytvořit zástupce Otevřít v novém okně - Vybráno %1$,d Vyberte \"%1$s\" Přesouvání %1$,d Kopírování %1$,d @@ -547,7 +545,6 @@ Zastavování… Není spuštěno URL - Nelze získat lokální IP adresu Kopírovat URL Kopírovat heslo Konfigurace @@ -557,7 +554,6 @@ Port Složka Root Povolit zapisování - Nastavení Rozhraní Barva motivu diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d57ca578f..70061c74e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -43,6 +43,7 @@ Anzeigen Überspringen Stopp + Systemvorgabe Unbekannt Anzeigen @@ -271,6 +272,9 @@ ,\u0020 Pfad darf nicht leer sein Ungültiger Pfad + Anzeigen und Sortieren + Liste + Raster Name Typ Größe @@ -287,7 +291,7 @@ Lesezeichen hinzufügen Verknüpfung erstellen In neuem Fenster öffnen - %1$,d ausgewählt + %1$,d “%1$s” auswählen Verschiebe %1$,d Kopiere %1$,d @@ -421,6 +425,10 @@ Gerätespeicher bearbeiten Name Pfad + DocumentsUI Verknüpfung hinzufügen + DocumentsUI Verknüpfung bearbeiten + Gib eine URI ein + Ungültige URI Externen Speicher hinzufügen Externen Speicher bearbeiten Name @@ -544,9 +552,13 @@ Beendet… Nicht gestartet URL - Lokale IP-Adresse konnte nicht abgerufen werden + Die Lokale IP Adresse ist unbekannt Kopiere URL Kopiere Passwort + Zu den Schnell-Einstellungen hinzufügen + \"FTP server\" ist zu den Schnell-Einstellungen hinzugefügt worden + \"FTP server\" ist schon in den Schnell-Einstellungen vorhanden + Fehler beim hinzufügen von \"FTP server\" zu den Schnell-Einstellungen Konfiguration Anonymer Login Benutzername @@ -554,9 +566,9 @@ Port Stammverzeichnis Erlaube Schreiben - Einstellungen Benutzeroberfläche + Sprache Thema Farbe Die am häufigsten vorkommende Farbe in der App Material Design 3 diff --git a/app/src/main/res/values-el/mime_types.xml b/app/src/main/res/values-el/mime_types.xml new file mode 100644 index 000000000..49a934ebf --- /dev/null +++ b/app/src/main/res/values-el/mime_types.xml @@ -0,0 +1,39 @@ + + + + + + Αρχείο + Πακέτο Android + %1$s αρχειοθήκη + %1$s ήχος + %1$s ημερολόγιο + %1$s πιστοποιητικό + %1$s έγγραφο + Ηλεκτρονική επαγγελματική κάρτα + Φάκελος + %1$s έγγραφο + %1$s ηλεκτρ-βιβλίο + %1$s μήνυμα email + %1$s γραμματοσειρά + %1$s αρχείο + %1$s εικόνα + Έγγραφο PDF + %1$s παρουσίαση + %1$s υπολογιστικό φύλλο + %1$s έγγραφο + Έγγραφο απλού κειμένου + %1$s βίντεο + Έγγραφο Word + Παρουσίαση PowerPoint + Υπολογιστικό φύλλο Excel + Χαρακτήρας συσκευής + Αποκλεισμός συσκευής + Pipe + Σύνδεσμος + Υποδοχή + Σύνδεσμος (κατεστραμμένος) + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 000000000..bc86648c2 --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,613 @@ + + + + + + + Αρχεία Υλικού + + Κλείσιμο + Αντιγραφή + Αποκοπή + Προεπιλογή + Διαγραφή + Απόρριψη + Επεξεργασία + Κενό + (Κενό) + Σφάλμα + Απόκρυψη + Εγκατάσταση + Συνέχεια Επεξεργασίας + Φόρτωση… + Ίσως αργότερα + Συγχώνευση + Χωρίς + Άνοιγμα συρταριού πλοήγησης + Επικόλληση + Ανανέωση + Επαναφόρτωση + Αφαίρεση + Μετονομασία + Αντικατάσταση + Επαναφορά + Επανάληψη + Αποθήκευση + Αναζήτηση + Επιλογή + Επιλογή όλων + Κοινή χρήση + Εμφάνιση + Παράλειψη + Stop + Άγνωστο + Προβολή + + + Δεν βρέθηκε εφαρμογή για να χειριστεί αυτή την ενέργεια + Άνοιγμα ρυθμίσεων + Δημιουργήθηκε συντόμευση + + + %1$,d byte + %1$,d bytes + + + Η εφαρμογή χρειάζεται άδεια πρόσβασης στα αρχεία. Κάντε κλικ στο “ΑΠΟΔΟΧΗ“ στο επόμενο παράθυρο διαλόγου του συστήματος. + Η εφαρμογή χρειάζεται άδεια πρόσβασης στα αρχεία. Παρακαλούμε παραχωρήστε την άδεια “Αποθήκευση“ στις ρυθμίσεις συστήματος. + Η εφαρμογή χρειάζεται πρόσβαση για τη διαχείριση όλων των αρχείων. Παρακαλούμε επιτρέψτε την πρόσβαση στην επερχόμενη ρύθμιση συστήματος. + + Ενέργειες στο παρασκήνιο + Λήψη ενεργειών με την εφαρμογή στο παρασκήνιο + Λειτουργίες αρχείου + Προβολή και έλεγχος λειτουργιών αρχείου + FTP server + Προβολή και έλεγχος του FTP server + + + Προετοιμασία για συμπίεση %1$,d αρχείο (%2$s) + Προετοιμασία για συμπίεση %1$,d αρχείων (%2$s) + + Συμπίεση “%1$s” σε “%2$s” + + Συμπίεση %1$,d αρχείο σε “%2$s” + Συμπίεση %1$,d αρχεία σε “%2$s” + + + Προετοιμασία αντιγραφής %1$,d αρχείο (%2$s) + Προετοιμασία αντιγραφής %1$,d αρχείων (%2$s) + + Αντιγραφή “%1$s” σε “%2$s” + + Αντιγραφή %1$,d αρχείο σε “%2$s” + Αντιγραφή %1$,d αρχεία σε “%2$s” + + + Προετοιμασία για εξαγωγή %1$,d αρχείο (%2$s) + Προετοιμασία για εξαγωγή %1$,d αρχείων (%2$s) + + Εξαγωγή “%1$s” σε “%2$s” + + Εξαγωγή %1$,d αρχείο σε “%2$s” + Εξαγωγή %1$,d αρχεία σε “%2$s” + + + Προετοιμασία για μετακίνηση %1$,d αρχείο (%2$s) + Προετοιμασία για μετακίνηση %1$,d αρχείων (%2$s) + + Μετακίνηση “%1$s” σε “%2$s” + + Μετακίνηση %1$,d αρχείο σε “%2$s” + Μετακίνηση %1$,d αρχεία σε “%2$s” + + %1$s / %2$s + %1$,d / %2$,d + + Προετοιμασία για διαγραφή %1$,d αρχείο + Προετοιμασία για διαγραφή %1$,d αρχεία + + Διαγραφή “%1$s” + + Διαγραφή %1$,d αρχείο + Διαγραφή %1$,d αρχεία + + + Προετοιμασία αλλαγής κατόχου για %1$,d αρχείο + Προετοιμασία αλλαγής κατόχου για %1$,d αρχεία + + Αλλαγή κατόχου για “%1$s” + + Αλλαγή κατόχου για %1$,d αρχείο + Αλλαγή κατόχου για %1$,d αρχεία + + + Προετοιμασία αλλαγής ομάδας για %1$,d αρχείο + Προετοιμασία αλλαγής ομάδας για %1$,d αρχεία + + Αλλαγή ομάδας για “%1$s” + + Αλλαγή ομάδας για %1$,d αρχείο + Αλλαγή ομάδας για %1$,d αρχεία + + + Προετοιμασία αλλαγής λειτουργίας για %1$,d αρχείο + Προετοιμασία αλλαγής λειτουργίας για %1$,d αρχεία + + Αλλαγή λειτουργίας για “%1$s” + + Αλλαγή λειτουργίας για %1$,d αρχείο + Αλλαγή λειτουργίας για %1$,d αρχεία + + + Προετοιμασία αλλαγής SELinux context για %1$,d αρχείο + Προετοιμασία αλλαγής SELinux context για %1$,d αρχεία + + Αλλαγή SELinux context για “%1$s” + + Αλλαγή SELinux context για %1$,d αρχείο + Αλλαγή SELinux context για %1$,d αρχεία + + + Προετοιμασία επαναφοράς SELinux context για %1$,d αρχείο + Προετοιμασία επαναφοράς SELinux context για %1$,d αρχεία + + Επαναφορά SELinux context για “%1$s” + + Επαναφορά SELinux context για %1$,d αρχείο + Επαναφορά SELinux context για %1$,d αρχεία + + %1$,d / %2$,d + Εγγραφή “%1$s” + Σφάλμα κατά τη δημιουργία + Υπήρξε σφάλμα δημιουργίας “%1$s”.\n%2$s + Αδύνατη η αντιγραφή ενός φακέλου στον εαυτό του + Αδύνατη η εξαγωγή φακέλου στον εαυτό του + Αδύνατη η μετακίνηση φακέλου στον εαυτό του + Ο φάκελος προορισμού βρίσκεται μέσα στο φάκελο προέλευσης. + Αδύνατη η αντιγραφή ενός αρχείου στον εαυτό του + Αδύνατη η εξαγωγή ενός αρχείου στον εαυτό του + Αδύνατη η μετακίνηση ενός αρχείου στον εαυτό του + The source file would be overwritten by the destination. + Αντικατάσταση αρχείου “%1$s”? + Υπάρχει ήδη ένα άλλο αρχείο με το ίδιο όνομα στο "%1$s".\nΗ αντικατάστασή του θα αντικαταστήσει το περιεχόμενό του. + Αρχικό αρχείο + Αντικατάσταση με + Συγχώνευση φακέλου “%1$s”? + Η συγχώνευση θα ζητήσει επιβεβαίωση πριν από την αντικατάσταση τυχόν αρχείων στο φάκελο που έρχονται σε σύγκρουση με τα αρχεία που αντιγράφονται. + Η συγχώνευση θα ζητήσει επιβεβαίωση πριν από την αντικατάσταση τυχόν αρχείων στο φάκελο που έρχονται σε σύγκρουση με τα αρχεία που εξάγονται. + Η συγχώνευση θα ζητήσει επιβεβαίωση πριν αντικαταστήσει τυχόν αρχεία στο φάκελο που έρχονται σε σύγκρουση με τα αρχεία που μετακινούνται. + Αρχικός φάκελος + Συγχώνευση με + Επιλογή νέου ονόματος για τον προορισμό + Νέο όνομα + Σφάλμα κατά τη συμπίεση “%1$s” + Υπήρξε σφάλμα συμπίεσης του αρχείου σε “%1$s”.\n%2$s + Σφάλμα κατά την αντιγραφή “%1$s” + Υπήρξε σφάλμα κατά την αντιγραφή του αρχείου στο “%1$s”.\n%2$s + Σφάλμα κατά την εξαγωγή “%1$s” + Υπήρξε σφάλμα κατά την εξαγωγή του αρχείου στο “%1$s”.\n%2$s + Σφάλμα κατά τη μετακίνηση “%1$s” + Υπήρξε σφάλμα κατά τη μετακίνηση του αρχείου στο “%1$s”.\n%2$s + Σφάλμα κατά τη διαγραφή + Υπήρξε σφάλμα κατά τη διαγραφή “%1$s”.\n%2$s + Σφάλμα κατά τη μετονομασία “%1$s” + Υπήρξε σφάλμα κατά τη μετονομασία του αρχείου σε “%1$s”.\n%2$s + Σφάλμα κατά την αλλαγή κατόχου για “%1$s” + Υπήρξε ένα σφάλμα αλλαγής του κατόχου σε “%1$s”.\n%2$s + Σφάλμα κατά την αλλαγή ομάδας για “%1$s” + Υπήρξε σφάλμα στην αλλαγή της ομάδας σε “%1$s”.\n%2$s + Σφάλμα κατά την αλλαγή λειτουργίας για “%1$s” + Υπήρξε σφάλμα κατά την αλλαγή του τρόπου λειτουργίας σε “%1$s”.\n%2$s + Σφάλμα κατά την αλλαγή SELinux context για “%1$s” + Υπήρξε σφάλμα κατά την αλλαγή του SELinux context σε “%1$s”.\n%2$s + Σφάλμα κατά την επαναφορά SELinux context + Υπήρξε σφάλμα κατά την επαναφορά του SELinux context για “%1$s”.\n%2$s + Σφάλμα κατά την εγγραφή + Υπήρξε σφάλμα κατά την εγγραφή “%1$s”.\n%2$s + Υπήρξε σφάλμα στη λήψη πληροφοριών για “%1$s”. + Προσάρτηση του "%1$s" ως read-write + Προσάρτηση “%1$s” ως read-write… + “%1$s” έχει προσαρτηθεί ξανά ως read-write + Εφαρμογή αυτής της ενέργειας σε όλα τα αρχεία + + Αυτό το αρχείο είναι ένα πακέτο Android. Θέλετε να το εγκαταστήσετε ή να προβάλετε τα περιεχόμενά του; + Άνοιγμα ως… + Άνοιγμα “%1$s” ως + Κείμενο + Εικόνα + Ήχος + Βίντεο + Φάκελος + Άλλο + “%1$s” είναι έτοιμο για εγκατάσταση + Πατήστε για εγκατάσταση + “%1$s” είναι έτοιμο για άνοιγμα + Πατήστε για άνοιγμα + Το όνομα αρχείου δεν μπορεί να είναι κενό + Άκυρο όνομα αρχείου + Υπάρχει ήδη ένα αρχείο με αυτό το όνομα + Διαγραφή “%1$s”? + Διαγραφή του φακέλου "%1$s" και των περιεχομένων του; + + Διαγραφή %1$,d αρχείο? + Διαγραφή %1$,d αρχεία? + + + Διαγραφή %1$,d φακέλου και του περιεχομένου του; + Διαγραφή %1$,d φακέλων και των περιεχομένων τους; + + + Διαγραφή %1$,d στοιχείου; + Διαγραφή %1$,d στοιχείων; + + Δημιουργία αρχειοθήκης + .zip + .tar.xz + .7z + Προστέθηκε σελιδοδείκτης + Νέο αρχείο + Νέος φάκελος + + Αρχεία + + Επιλογή αρχείου + Επιλογή αρχείων + + + Επιλογή φακέλου + Επιλογή φακέλων + + + %1$,d φάκελος + %1$,d φάκελοι + + + %1$,d αρχείο + %1$,d αρχεία + + ,\u0020 + Η διαδρομή δεν μπορεί να είναι κενή + Άκυρη διαδρομή + Όνομα + Τύπο + Μέγεθος + Τελευταία τροποποίηση + Αύξουσα + Πρώτα οι φάκελοι + Νέο παράθυρο + Μετάβαση επάνω + Μετάβαση σε + Εμφάνιση κρυφών αρχείων + Αντιγραφή διαδρομής + Άνοιγμα στο terminal + Προσθήκη σελιδοδείκτη + Δημιουργία συντόμευσης + Άνοιγμα σε νέο παράθυρο + Επιλογή “%1$s” + Μετακίνηση %1$,d + Αντιγραφή %1$,d + Εξαγωγή %1$,d + Εξαγωγή εδώ + Χωρίς αρχεία + Αρχείο + Φάκελος + + \u0020\u0020\u0020\u0020 + Άνοιγμα με + Εξαγωγή + Συμπίεση + Ιδιότητες + + “%1$s” ιδιότητες + Βασικές + Όνομα + Τύπος + %1$s (%2$s) + Σύνδεση με %1$s (%2$s) + Προορισμός σύνδεσης + Μέγεθος + %1$s (%2$s) + Περιεχόμενα + + %1$,d στοιχείο, με μέγεθος %2$s + %1$,d στοιχεία, συνολικά %2$s + + Τελευταία τροποποίηση + Γονικός φάκελος + Αρχειοθέτηση αρχείου + Καταχώρηση αρχείου + Ελεύθερος χώρος + Δικαιώματα + Κάτοχος + Ομάδα + %1$s (%2$d) + Αλλαγή κατόχου + Αλλαγή ομάδας + Εισάγετε ένα όνομα ή ID + Σύστημα + Λειτουργία + %1$s (%2$04o) + Αλλαγή λειτουργίας + Άλλα + + Ανάγνωση + Εγγραφή + Εκτέλεση + + + Ανάγνωση + Εγγραφή + Αναζήτηση + + Ειδικά + + Ορισμός UID + Ορισμός GID + Sticky bit + + Χωρίς προσθήκη “Εκτέλεση“ για τα κλεισμένα αρχεία + SELinux Context + Αλλαγή SELinux context + Επαναφορά + Εφαρμογή σε κλεισμένα αρχεία + Εικόνα + Διαστάσεις + %1$d \u00d7 %2$d + Λήφθηκε σε + Συντεταγμένες + %1$.3f, %2$.3f + Τοποθεσία + Υψόμετρο + %1$,.3f m + Κάμερα + %1$s %2$s + Διάφραγμα + Ταχύτητα κλείστρου + Εστιακό μήκος + %1$.2f mm + ISO ισοδύναμο + ISO %1$d + Λογισμικό + Περιγραφή + Δημιουργός + Πνευματικά δικαιώματα + Ήχος + Τίτλος + Καλλιτέχνης + Άλμπουμ + Άλμπουμ καλλιτέχνη + Συνθέτης + Δίσκος + Κομμάτι + Έτος + Είδος + Διάρκεια + Ρυθμός μετάδοσης + %1$d kbps + Ρυθμός δειγματοληψίας + %1$d Hz + Βίντεο + APK + Όνομα + Όνομα πακέτου + Έκδοση + %1$s (%2$d) + Ελάχιστη έκδοση συστήματος + Έκδοση συστήματος προορισμού + %1$s (%2$s, %3$d) + Δικαιώματα + Δεν ζητήθηκαν δικαιώματα + + %1$,d απαιτούμενο δικαίωμα + %1$,d απαιτούμενα δικαιώματα + + Υπογραφές + Χωρίς έγκυρες υπογραφές + Παλιές υπογραφές + + Root + Αποθήκευση + Καμία αποθήκευση + Προσθήκη αποθήκευσης + Εξωτερική αποθήκευση + FTP διακομιστής + SFTP διακομιστής + SMB διακομιστής + Επεξεργασία αποθήκευσης συσκευής + Όνομα + Διαδρομή + Προσθήκη εξωτερικής αποθήκευσης + Επεξεργασία εξωτερικής αποθήκευσης + Όνομα + URI + Διαδρομή + Επεξεργασία διακομιστή FTP + Προσθήκη διακομιστή FTP + Hostname + Εισάγετε ένα hostname + Άκυρο hostname + Θύρα + Άκυρη θύρα + Διαδρομή + Μπορεί να μείνει κενό + Όνομα + Χρήση hostname + Πρωτόκολλο + Πιστοποίηση + + Κωδικός + Ανώνυμα + + Όνομα χρήστη + Εισάγετε όνομα χρήστη + Κωδικός + Λειτουργία + + Ενεργός + Παθητικός + + Κωδικοποίηση + Σύνδεση και προσθήκη + Προσθήκη + Επεξεργασία διακομιστή SFTP + Προσθήκη διακομιστή SFTP + Hostname + Εισάγετε ένα hostname + Άκυρο hostname + Θύρα + Άκυρη θύρα + Διαδρομή + Μπορεί να μείνει κενό + Όνομα + Χρήση hostname + Πιστοποίηση + + Κωδικός + Δημόσιο κλειδί + + Όνομα χρήστη + Εισάγετε όνομα χρήστη + Κωδικός + Ιδιωτικό κλειδί + Άνοιγμα αρχείου + Εισαγωγή ιδιωτικού κλειδιού + Άκυρο ιδιωτικό κλειδί + Κωδικός ιδιωτικού κλειδιού + Μπορεί να μείνει κενό + Άκυρος κωδικός ιδιωτικού κλειδιού + Σύνδεση και προσθήκη + Προσθήκη + Αναζήτηση διακομιστών SMB… + Προσθήκη χειροκίνητα + Επεξεργασία διακομιστή SMB + Προσθήκη διακομιστή SMB + Hostname + Εισάγετε ένα hostname + Άκυρο hostname + Θύρα + Άκυρη θύρα + Διαδρομή + Μπορεί να μείνει κενό + Όνομα + Χρήση hostname + Πιστοποίηση + + Κωδικός + Επισκέπτης + Ανώνυμα + + Όνομα χρήστη + Εισάγετε όνομα χρήστη + Κωδικός + Τομέας + Σύνδεση και προσθήκη + Προσθήκη + + %1$s ελεύθερα από %2$s + Προσθήκη αποθήκευσης… + Ειδοποιήσεις + DCIM + Έγγραφα + Λήψεις + Ταινίες + Μουσική + Ειδοποιήσεις + Εικόνες + Podcasts + Ήχοι κλήσεων + QQ + TIM + WeChat + Σελιδοδείκτης φακέλου + Όνομα + Διαδρομή + Προβολή αρχειοθήκης + Προβολή εικόνων + %1$,d/%2$,d + Επεξεργαστής κειμένου + %1$s + *%1$s + Αποθηκεύτηκε + Σίγουρα θέλετε να επαναλάβετε τη φόρτωση; Οι μη αποθηκευμένες αλλαγές σε αυτό το έγγραφο θα χαθούν. + Κωδικοποίηση + Σίγουρα θέλετε να απορρίψετε τις μη αποθηκευμένες αλλαγές σε αυτό το έγγραφο; + + Διακομιστής FTP + Κατάσταση + Εκκίνηση… + Εκτελείται + Διακόπτεται… + Δεν ξεκίνησε + URL + Αντιγραφή URL + Αντιγραφή κωδικού + Διαμόρφωση + Ανώνυμη σύνδεση + Όνομα χρήστη + Κωδικός + Θύρα + Root φάκελος + Αποδοχή εγγραφής + Ρυθμίσεις + Διεπαφή + Χρώμα θέματος + Χρώμα που εμφανίζεται συχνότερα στην εφαρμογή + Σχεδίαση υλικού 3 + Νυχτερινή λειτουργία + + Ακολουθεί το σύστημα + Off + On + Βάση ώρας + Βάση εξοικον. μπαταρίας + + Νυχτερινή λειτουργία Μαύρο + Κινούμενη εικόνα λίστας αρχείων + Εμφάνιση μακροσκελούς ονόματος αρχείου + + Ελλειψομετρία στην αρχή + Ελλειψομετρία στη μέση + Ελλειψομετρία στο τέλος + Μαρκίζα + + Συμπεριφορά + Προεπιλεγμένος φάκελος + Τυπικοί φάκελοι + Χωρίς ενεργούς τυπικούς φακέλους + Στιγμιότυπα οθόνης + Σελιδοδείκτης φακέλων + Χωρίς σελιδοδείκτη φακέλων + Λειτουργία πρόσβασης Root + + Έχετε %1$,d αρχείο σε εκτέλεση και αλλαγή της λειτουργίας πρόσβασης root μπορεί να προκαλέσει τώρα μη αναμενόμενο σφάλμα. Είστε βέβαιοι ότι θέλετε να την αλλάξετε; + Έχετε %1$,d αρχεία σε εκτέλεση και αλλαγή της λειτουργίας πρόσβασης root μπορεί να προκαλέσει τώρα μη αναμενόμενο σφάλμα. Είστε βέβαιοι ότι θέλετε να την αλλάξετε; + + + Κανονική πρόσβαση μόνο + Αυτόματη + Πρόσβαση μόνο Root + + Κωδικοπ/ση αρχειοθέτησης ονόμ. αρχείου + Άνοιγμα πακέτου Android + + Εγκατάσταση + Προβολή περιεχομένων + Ερώτηση για ενέργεια + + Εμφάνιση μικρογραφίας για αρχεία PDF + Μπορεί να κάνει την εφαρμογή ασταθή σε παλαιότερες εκδόσεις Android + Ανάγν. απόμακρων αρχ. για μικρογραφία + + Σχετικά + Έκδοση + Προβολή στο GitHub + Άδειες + Πολιτική απορρήτου + Συντάκτης + Hai Zhang + Ακολουθήστε στο GitHub + Ακολουθήστε στο Twitter + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 31026e510..a88485997 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -26,6 +26,7 @@ Quizás más tarde Fusionar Ninguno + Abrir barra de navegación Pegar Actualizar Recargar @@ -42,6 +43,7 @@ Mostrar Saltar Parar + Por defecto del sistema Desconocido Ver @@ -52,11 +54,14 @@ %1$,d byte + %1$,d bytes %1$,d bytes La aplicación necesita permiso para acceder a los archivos. Por favor, aprieta \"PERMITIR\" en el diálogo del sistema. La aplicación necesita permiso para acceder a los archivos. Por favor, concede el permiso de \"Almacenamiento\" en la configuración del sistema. + La aplicación necesita administrar todos los archivos. Por favor permita el acceso en la siguiente configuración del sistema. + Acciones en segundo plano Realiza acciones mientras la aplicación está en segundo plano Operaciones de archivos @@ -66,94 +71,114 @@ Preparando para comprimir %1$,d archivos (%2$s) + Preparándose para comprimir %1$,d archivos (%2$s) Preparándose para comprimir %1$,d archivos (%2$s) Comprimiendo \"%1$s\" en \"%2$s\" Comprimiendo %1$,d archivos en \"%2$s\" + Comprimiendo %1$,d archivos en \"%2$s\" Comprimiendo %1$,d archivos en \"%2$s\" Preparando para copiar %1$,d archivos (%2$s) + Preparándose para copiar %1$,d archivos (%2$s) Preparándose para copiar %1$,d archivos (%2$s) Copiando \"%1$s\" a \"%2$s\" Copiando %1$,d archivos a \"%2$s\" + Copiando %1$,d archivos a \"%2$s\" Copiando %1$,d archivos a \"%2$s\" Preparando para extraer %1$,d archivos (%2$s) + Preparando para extraer %1$,d archivos (%2$s) Preparando para extraer %1$,d archivos (%2$s) Extrayendo \"%1$s\" a \"%2$s\" Extrayendo %1$,d archivos a \"%2$s\" + Extrayendo %1$,d archivos a \"%2$s\" Extrayendo %1$,d archivos a \"%2$s\" Preparando para mover %1$,d archivos (%2$s) + Preparando para mover %1$,d archivos (%2$s) Preparando para mover %1$,d archivos (%2$s) Moviendo \"%1$s\" a \"%2$s\" Moviendo %1$,d archivos a \"%2$s\" + Moviendo %1$,d archivos a \"%2$s\" Moviendo %1$,d archivos a \"%2$s\" %1$s / %2$s %1$,d / %2$,d Preparando para eliminar %1$,d archivos + Preparando para eliminar %1$,d archivos Preparando para eliminar %1$,d archivos Eliminando \"%1$s\" Eliminando %1$,d archivos + Eliminando %1$,d archivos Eliminando %1$,d archivos Preparando para cambiar el propietario de %1$,d archivos + Preparando para cambiar el propietario de %1$,d archivos Preparando para cambiar el propietario de %1$,d archivos Cambiando el propietario de \"%1$s\" Cambiando el propietario de %1$,d archivos + Cambiando el propietario de %1$,d archivos Cambiando el propietario de %1$,d archivos Preparando para cambiar el grupo de %1$,d archivos + Preparando para cambiar el grupo de %1$,d archivos Preparando para cambiar el grupo de %1$,d archivos Cambiando el grupo de \"%1$s\" Cambiando el grupo de %1$,d archivos + Cambiando el grupo de %1$,d archivos Cambiando el grupo de %1$,d archivos Preparando para cambiar el modo de %1$,d archivos + Preparando para cambiar el modo de %1$,d archivos Preparando para cambiar el modo de %1$,d archivos Cambiando el modo de \"%1$s\" Cambiando el modo de %1$,d archivos + Cambiando el modo de %1$,d archivos Cambiando el modo de %1$,d archivos Preparándose para cambiar el contexto de SELinux de %1$,d archivos + Preparándose para cambiar el contexto de SELinux de %1$,d archivos Preparándose para cambiar el contexto de SELinux de %1$,d archivos Cambiando el contexto SELinux de \"%1$s\" Cambiando el contexto de SELinux de %1$,d archivos + Cambiando el contexto de SELinux de %1$,d archivos Cambiando el contexto de SELinux de %1$,d archivos Preparándose para restaurar el contexto de SELinux de %1$,d archivos + Preparándose para restaurar el contexto de SELinux de %1$,d archivos Preparándose para restaurar el contexto de SELinux de %1$,d archivos Restaurando el contexto de SELinux de \"%1$s\" Restaurando el contexto de SELinux de %1$,d archivos + Restaurando el contexto de SELinux de %1$,d archivos Restaurando el contexto de SELinux de %1$,d archivos %1$,d / %2$,d @@ -239,18 +264,22 @@ Archivos Seleccionar un archivo + Seleccionar archivos Seleccionar archivos Seleccionar una carpeta + Seleccionar carpetas Seleccionar carpetas %1$,d carpeta + %1$,d carpetas %1$,d carpetas %1$,d archivo + %1$,d archivos %1$,d archivos ,\u0020 @@ -260,7 +289,6 @@ Última modificación Ascendente Carpetas primero - Sólo para esta carpeta Nueva ventana Subir Mostrar archivos ocultos @@ -269,7 +297,6 @@ Agregar marcador Crear atajo Abrir en una nueva ventana - %1$,d seleccionado Seleccionar \"%1$s\" Moviendo %1$,d Copiando %1$,d @@ -297,6 +324,7 @@ Contenido %1$,d artículo, con un tamaño %2$s + %1$,d artículos, en total%2$s %1$,d artículos, en total%2$s Última modificación @@ -386,6 +414,7 @@ No se requieren permisos No se requieren permisos + %1$,d permisos requeridos %1$,d permisos requeridos Firmas @@ -491,7 +520,6 @@ Deteniendo… Sin iniciar URL - No se puede obtener la dirección IP local Copiar URL Copiar contraseña Configuración @@ -501,7 +529,6 @@ Puerto Carpeta raíz Permitir escritura - Ajustes Interfaz Color de tema @@ -533,6 +560,7 @@ Modo de acceso administrativo Tienes %1$,d operaciones de archivo ejecutándose y cambiar el modo de acceso administrativo puede causar errores inesperados. ¿Estás seguro que quieres cambiarlo ahora? + Tienes %1$,d operaciones de archivo ejecutándose y cambiar el modo de acceso administrativo puede causar errores inesperados. ¿Estás seguro que quieres cambiarlo ahora? Tienes %1$,d operaciones de archivo ejecutándose y cambiar el modo de acceso administrativo puede causar errores inesperados. ¿Estás seguro que quieres cambiarlo ahora? diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index a2457ad2c..bee710ffc 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -255,7 +255,6 @@ Azken aldaketa Gorantza Karpetak aurretik - Karpeta honetan bakarrik Leiho berria Joan gora Erakutsi ezkutatutako fitxategiak @@ -264,7 +263,6 @@ Gehitu laster-marka Sortu laster-marka Ireki leiho berrian - %1$,d hautatuta Hautatu “%1$s” %1$,d lekuz aldatzen %1$,d kopiatzen @@ -363,7 +361,6 @@ Gelditzen… Hasi gabe URLa - Ezin izan da IP lokala eskuratu Kopiatu URLa Kopiatu pasahitza Konfigurazioa @@ -373,7 +370,6 @@ Ataka Erro karpeta Baimendu idaztea - Ezarpenak Interfazea Gauerako modua diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 23fe4617e..f52201806 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -28,6 +28,7 @@ شاید بعداً ادغام هیچ + گشودن کشوی ناوبری جای‌گذاری نوسازی بار کردن دوباره @@ -232,6 +233,18 @@ پرونده‌ای با این نام هم‌اکنون موجود است مورد «⁨%1$s⁩» حذف شود؟ شاخهٔ «⁨%1$s⁩» و محتویاتش حذف شود؟ + + تعداد %1$,d پرونده حذف شود؟ + تعداد %1$,d پرونده حذف شوند؟ + + + تعداد %1$,d شاخه و محتویاتشان حذف شود؟ + تعداد %1$,d شاخه و محتویاتشان حذف شوند؟ + + + تعداد %1$,d مورد حذف شود؟ + تعداد %1$,d مورد حذف شوند؟ + ساختن بایگانی .zip .tar.xz @@ -258,22 +271,23 @@ %1$,d پرونده ،\u0020 + مسیر نمی‌تواند خالی باشد + مسیر نامعتبر است نام گونه اندازه آخرین تغییر بالا رونده ابتدا شاخه‌ها - تنها برای این شاخه پنجرهٔ جدید برو بالا + رفتن به نمایش پرونده‌های پنهان رونوشت مسیر گشودن در پایانه افزودن نشانک ایجاد میان‌بر گشودن در پنجرهٔ جدید - %1$,d گزیده گزینش «⁨%1$s⁩» در حال جابه‌جایی %1$,d در حال رونوشت %1$,d @@ -401,6 +415,7 @@ بدون ذخیره‌ساز افزودن ذخیره‌ساز ذخیره‌ساز خارجی + کارساز اف‌تی‌پی کارساز اس‌اف‌تی‌پی کارساز اس‌ام‌بی ویرایش ذخیره‌ساز افزاره @@ -411,14 +426,43 @@ نام نشانی مسیر + ویرایش کارساز اف‌تی‌پی + افزودن کارساز اف‌تی‌پی + نام میزبان + نام میزبان را وارد کنید + نام میزبان نامعتبر است + درگاه + درگاه نامعتبر است + مسیر + می‌تواند خالی بماند + نام + استفاده از نام میزبان + شیوه‌نامه + احراز هویت + + گذرواژه + ناشناس + + نام کاربری + نام کاربری را وارد کنید + گذرواژه + حالت + + فعّال + منفعل + + رمزگذاری + اتّصال و افزودن + افزودن ویرایش کارساز اس‌اف‌تی‌پی افزودن کارساز اس‌اف‌تی‌پی نام میزبان یک نام میزبان وارد کنید + نام میزبان نامعتبر است درگاه درگاه نامعتبر مسیر - می‌توان خالی گذاشت + می‌تواند خالی بماند نام استفاده از نام میزبان احراز هویت @@ -433,6 +477,9 @@ گشودن پرونده یک کلید خصوصی وارد کنید کلید خصوصی نامعتبر + گذرواژهٔ کلید خصوصی + می‌تواند خالی بماند + گذرواژهٔ کلید خصوصی نامعتبر است اتّصال و افزودن افزودن در حال جست‌وجو برای کارسازهای اس‌ام‌بی… @@ -441,10 +488,11 @@ افزودن کارساز اس‌ام‌بی نام میزبان یک نام میزبان وارد کنید + نام میزبان نامعتبر است درگاه درگاه نامعتیر مسیر - می‌توان خالی گذاشت + می‌تواند خالی بماند نام استفاده از نام میزبان احراز هویت @@ -478,6 +526,7 @@ شاخهٔ نشانک نام مسیر + نمایشگر بایگانی نمایشگر تصویر %1$,d/%2$,d ویرایشگر متن @@ -495,7 +544,6 @@ در حال توقّف… شروع نشد نشانی - نمی‌توان نشانی آی‌پی محلّی را بازیابی کرد رونوشت نشانی رونوشت گذرواژه پیکربندی @@ -505,7 +553,6 @@ درگاه شاخهٔ ریشه اجازهٔ نوشتن - تنظیمات رابط رنگ زمینه diff --git a/app/src/main/res/values-fi/mime_types.xml b/app/src/main/res/values-fi/mime_types.xml new file mode 100644 index 000000000..4a22216c6 --- /dev/null +++ b/app/src/main/res/values-fi/mime_types.xml @@ -0,0 +1,31 @@ + + + + + + Tiedosto + Android-asennuspakkaus + %1$s arkisto + %1$s ääni + %1$s kalenteri + %1$s sertifikaatti + %1$s dokumentti + Kansio + %1$s dokumentti + %1$s e-kirja + %1$s sähköpostiviesti + %1$s kirjasin + %1$s tiedosto + %1$s kuva + PDF-dokumentti + %1$s esitys + %1$s laskentataulukko + %1$s dokumentti + Pelkkä teksti-dokumentti + %1$s video + Word-dokumentti + PowerPoint-esitys + Excel-laskentataulukko diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 000000000..bbde3a0fa --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,607 @@ + + + + + + + Material Files + + Sulje + Kopioi + Leikkaa + Oletus + Poista + Hylkää + Muokkaa + Tyhjennä + (tyhjä) + Virhe + Piilota + Asenna + Jatka muokkaamista + Ladataan… + Ehkä myöhemmin + Yhdistä + Ei mitään + Avaa navigointipalkki + Liitä + Päivitä + Lataa uudelleen + Poista + Nimeä uudelleen + Korvaa + Nollaa + Yritä uudelleen + Tallenna + Etsi + Valitse + Valitse kaikki + Jaa + Näytä + Ohita + Pysäytä + Järjestelmän oletus + Tuntematon + + Toiminnalle ei löytynyt sovellusta + Avaa asetukset + Pikakuvake luotu + + + %1$,d tavu + %1$,d tavua + + + Sovellus tarvitsee tiedostojen käyttöoikeuden. Klikkaa “HYVÄKSY” seuraavassa järjestelmän ikkunassa. + Sovellus tarvitsee tiedostojen käyttöoikeuden. Anna käyttöoikeus tallennustilan käyttöön järjestelmän asetuksista. + Sovellus tarvitsee kaikkien tiedostojen käyttöoikeuden. Anna käyttöoikeus seuraavaksi avautuvissa järjestelmän asetuksissa. + + Toiminnot taustalla + Suorita toimintoja sovelluksen ollessa taustalla + Tiedostotoiminnot + Näytä ja hallinnoi tiedostotoimintoja + FTP-palvelin + Näytä ja hallinnoi FTP-palvelinta + + + Valmistaudutaan pakkaamaan %1$,d tiedosto (%2$s) + Valmistaudutaan pakkaamaan %1$,d tiedostoa (%2$s) + + Pakataan “%1$s” kohteeseen “%2$s” + + Pakataan %1$,d tiedosto kohteeseen “%2$s” + Pakataan %1$,d tiedostoa kohteeseen “%2$s” + + + Valmistaudutaan kopioimaan %1$,d tiedosto (%2$s) + Valmistaudutaan kopioimaan %1$,d tiedostoa (%2$s) + + Kopioidaan “%1$s” kohteeseen “%2$s” + + Kopioidaan tiedosto %1$,d kohteeseen “%2$s” + Kopioidaan %1$,d tiedostoa kohteeseen “%2$s” + + + Valmistaudutaan purkamaan %1$,d tiedosto (%2$s) + Valmistaudutaan purkamaan %1$,d tiedostoa (%2$s) + + Puretaan “%1$s” kohteeseen “%2$s” + + Puretaan %1$,d tiedosto kohteeseen “%2$s” + Puretaan %1$,d tiedostoa kohteeseen “%2$s” + + + Valmistaudutaan siirtämään %1$,d tiedosto (%2$s) + Valmistaudutaan siirtämään %1$,d tiedostoa (%2$s) + + Siirretään “%1$s” kohteeseen “%2$s” + + Siirretään %1$,d tiedosto kohteeseen “%2$s” + Siirretään %1$,d tiedostoa kohteeseen “%2$s” + + %1$s / %2$s + %1$,d / %2$,d + + Valmistaudutaan poistamaan %1$,d tiedosto + Valmistaudutaan poistamaan %1$,d tiedostoa + + Poistetaan “%1$s” + + Poistetaan %1$,d tiedosto + Poistetaan %1$,d tiedostoa + + + Valmistaudutaan vaihtamaan %1$,d tiedoston omistajaa + Valmistaudutaan vaihtamaan %1$,d tiedoston omistajaa + + Vaihdetaan kohteen “%1$s” omistajaa + + Vaihdetaan %1$,d tiedoston omistajaa + Vaihdetaan %1$,d tiedoston omistajaa + + + Valmistaudutaan vaihtamaan %1$,d tiedoston ryhmää + Valmistaudutaan vaihtamaan %1$,d tiedoston ryhmää + + Vaihdetaan kohteen “%1$s” ryhmää + + Vaihdetaan %1$,d tiedoston ryhmää + Vaihdetaan %1$,d tiedoston ryhmää + + + Valmistaudutaan vaihtamaan %1$,d tiedoston tilaa + Valmistaudutaan vaihtamaan %1$,d tiedoston tilaa + + Vaihdetaan kohteen “%1$s” tilaa + + Vaihdetaan %1$,d tiedoston tilaa + Vaihdetaan %1$,d tiedoston tilaa + + + Valmistaudutaan muuttamaan SELinux-konteksti %1$,d:lle tiedostolle + Valmistaudutaan muuttamaan SELinux-konteksti %1$,d:lle tiedostolle + + Muutetaan SELinux-konteksti kohteelle “%1$s” + + Muutetaan SELinux-konteksti %1$,d:lle tiedostolle + Muutetaan SELinux-konteksti %1$,d:lle tiedostolle + + + Valmistaudutaan palauttamaan SELinux-konteksti %1$,d:lle tiedostolle + Valmistaudutaan palauttamaan SELinux-konteksti %1$,d:lle tiedostolle + + Palautetaan SELinux-konteksti kohteelle “%1$s” + + Palautetaan SELinux-konteksti %1$,d:lle tiedostolle + Palautetaan SELinux-konteksti %1$,d:lle tiedostolle + + %1$,d / %2$,d + Tallennetaan “%1$s” + Virhe tiedoston luomisessa + Tapahtui virhe luotaessa “%1$s”.\n%2$s + Kansiota ei voida kopioida itsensä sisälle + Kansiota ei voida purkaa itsensä sisälle + Kansiota ei voida siirtää itsensä sisälle + Kohdehakemisto on lähdehakemiston sisällä + Tiedostoa ei voida kopioida itsensä päälle + Tiedostoa ei voida purkaa itsensä päälle + Tiedostoa ei voida siirtää itsensä päälle + Kohdetiedosto korvaisi lähdetiedoston + Korvataanko tiedosto “%1$s”? + Samanniminen tiedosto on jo olemassa hakemistossa “%1$s”.\nKorvaaminen hävittää sen sisällön. + Alkuperäinen tiedosto + Korvataan tiedostolla + Yhdistetäänkö kansio “%1$s”? + Alkuperäinen kansio + Yhdistä kansioon + Valitse uusi nimi kohteelle + Uusi nimi + Virhe pakattaessa “%1$s” + Tapahtui virhe tiedostoa kohteeseen “%1$s” pakatessa.\n%2$s + Virhe kopioitaessa “%1$s” + Tapahtui virhe kopioitaessa tiedostoa kohteeseen “%1$s”.\n%2$s + Virhe purkaessa “%1$s” + Tapahtui virhe purkaessa tiedostoa kohteeseen “%1$s”.\n%2$s + Virhe siirtäessä “%1$s” + Tapahtui virhe siirtäessä tiedostoa kohteeseen “%1$s”.\n%2$s + Virhe poistossa + Tapahtui virhe poistaessa “%1$s”.\n%2$s + Virhe uudelleennimetessä “%1$s” + Tapahtui virhe nimetessä tiedostoa nimelle “%1$s”.\n%2$s + Virhe kohteen “%1$s” omistajaa vaihtaessa + Tapahtui virhe vaihtaessa omistajaa “%1$s”.\n%2$s + Virhe ryhmän vaihtamisessa kohteelle “%1$s” + Tapahtui virhe vaihtaessa ryhmäksi “%1$s”.\n%2$s + Virhe SE-Linux-kontekstin palauttamisessa + Tapahtui virhe palauttaessa SELinux-kontekstia kohteelle “%1$s”.\n%2$s + Virhe kirjoittamisessa + Tapahtui virhe kirjoittaessa “%1$s”.\n%2$s + Tapahtui virhe hakiessa tietoa kohteesta “%1$s”. + Suorita toiminto kaikille tiedostoille + + Tämä tiedosto on Android-asennustiedosto. Haluatko asentaa sen vai nähdä sen sisällön? + Avaa… + Avaa “%1$s”… + Teksti + Kuva + Audio + Video + Kansio + Muu + “%1$s” on valmiina asennettavaksi + Napauta asentaaksesi + “%1$s” on valmiina avattavaksi + Napauta avataksesi + Tiedostonimi ei voi olla tyhjä + Virheellinen tiedostonimi + Samanniminen tiedosto on jo olemassa + Poistetaanko “%1$s”? + Poistetaanko kansio “%1$s” sisältöineen? + + Poistetaanko %1$,d tiedosto? + Poistetaanko %1$,d tiedostoa? + + + Poistetaanko %1$,d kansio sisältöineen? + Poistetaanko %1$,d kansiota sisältöineen? + + + Poistetaanko %1$,d kohde? + Poistetaanko %1$,d kohdetta? + + Luo arkisto + .zip + .tar.xz + .7z + Kirjanmerkki lisätty + Uusi tiedosto + Uusi kansio + + Tiedostot + + Valitse tiedosto + Valitse tiedostot + + + Valitse kansio + Valitse kansiot + + + %1$,d kansio + %1$,d kansiota + + + %1$,d tiedosto + %1$,d tiedostoa + + ,\u0020 + Polku ei voi olla tyhjä + Virheellinen polku + Näkymä ja lajittelu + Lista + Ruudukko + Nimi + Tyyppi + Koko + Viimeksi muokattu + Nouseva + Kansiot ensin + Vain tälle kansiolle + Uusi ikkuna + Mene ylös + Mene… + Näytä piilotetut tiedostot + Kopioi polku + Avaa päätteessä + Lisää kirjanmerkki + Luo pikakuvake + Avaa uudessa ikkunassa + %1$,d + Valitse “%1$s” + Siirretään %1$,d + Kopioidaan %1$,d + Puretaan %1$,d + Pura tähän + Ei tiedostoja + Tiedosto + Kansio + + \u0020\u0020\u0020\u0020 + Avaa… + Pura + Pakkaa + Ominaisuudet + + “%1$s” ominaisuudet + Perustiedot + Nimi + Tyyppi + %1$s (%2$s) + Linkki kohteeseen %1$s (%2$s) + Linkin kohde + Koko + %1$s (%2$s) + Sisältö + + %1$,d kohde kooltaan %2$s + %1$,d kohdetta, yhteensä %2$s + + Viimeksi muokattu + Ylähakemisto + Vapaata tilaa + Käyttöoikeudet + Omistaja + Ryhmä + %1$s (%2$d) + Vaihda omistajaa + Vaihda ryhmää + Syötä nimi tai ID + Järjestelmä + %1$s (%2$04o) + Muu + + Luku + Kirjoitus + Suoritus + + + Luku + Kirjoitus + Haku + + + Aseta UID + Aseta GID + Sticky bit + + SELinux-konteksti + Vaihda SELinux-kontekstia + Palauta + Kuva + Mitat + %1$d \u00d7 %2$d + Ottopäivä + Koordinaatit + %1$.3f, %2$.3f + Sijainti + Korkeus + %1$,.3f m + Kamera + %1$s %2$s + Aukkokoko + Suljinnopeus + Polttoväli + %1$.2f mm + ISO %1$d + Ohjelmisto + Kuvaus + Luoja + Copyright + Audio + Nimi + Artisti + Albumi + Albumin artisti + Säveltäjä + Levy + Kappale + Vuosi + Genre + Kesto + Bitrate + %1$d kbps + Näytteenottotaajuus + %1$d Hz + Video + APK + Nimi + Pakkauksen nimi + Versio + %1$s (%2$d) + Järestelmän vähimmäisversio + Järjestelmän kohdeversio + %1$s (%2$s, %3$d) + Käyttöoikeudet + Ei pyydettäviä käyttöoikeuksia + + %1$,d pyydettävä käyttöoikeus + %1$,d pyydettävää käyttöoikeutta + + Allerkirjoitukset + Ei kelvollisia allekirjoituksia + Vanhat allekirjoitukset + + Root + Tallennustila + Ei tallennustilaa + Lisää tallennustila + Ulkoinen tallennustila + FTP-palvelin + SFTP-palvelin + SMB-palvelin + Muokkaa laitteen tallennustilaa + Nimi + polku + Lisää DocumentsUI-pikakuvake + Muokkaa DocumentsUI-pikakuvaketta + Syötä URI + Virheellinen URI + Lisää ulkoinen tallennustila + Muokkaa ulkoista tallennustilaa + Nimi + URI + Polku + Muokkaa FTP-palvelinta + Lisää FTP-palvelin + Isäntänimi + Syötä isäntänimi + Virheellinen isäntänimi + Portti + Virheellinen portti + Polku + Voidaan jättää tyhjäksi + Nimi + Käytä isäntänimeä + Protokolla + Tunnistautuminen + + Salasana + Anonyymi + + Käyttäjänimi + Syötä käyttäjänimi + Salasana + Tila + + Aktiivinen + Passiivinen + + Koodaus + Yhdistä ja lisää + Lisää + Muokkaa SFTP-palvelinta + Lisää SFTP-palvelin + Isäntänimi + Syötä isäntänimi + Virheellinen isäntänimi + Portti + Virheellinen portti + Polku + Voidaan jättää tyhjäksi + Nimi + Käytä isäntänimeä + Tunnistautuminen + + Salasana + Julkinen avain + + Käyttäjänimi + Syötä käyttäjänimi + Salasana + Yksityinen avain + Avaa tiedosto + Syötä yksityinen avain + Virheellinen yksityinen avain + Ykstityisen avaimen salasana + Voidaan jättää tyhjäksi + Virheellinen yksityisen avaimen salasana + Yhdistä ja lisää + Lisää + Etsitään SMB-palvelimia… + Lisää manuaalisesti + Muokkaa SMB-palvelinta + Lisää SMB-palvelin + Isäntänimi + Syötä isäntänimi + Virheellinen isäntänimi + Portti + Virheellinen portti + Polku + Voidaan jättää tyhjäksi + Nimi + Käytä isäntänimeä + Tunnistautuminen + + Salasana + Vieras + Anonyymi + + Käyttäjänimi + Syötä käyttäjänimi + Salasana + Domain + Yhdistä ja lisää + Lisää + + %1$s/%2$s vapaana + Lisää tallennustila… + Hälytykset + DCIM + Dokumentit + Lataukset + Elokuvat + Musiikki + Ilmoitukset + Kuvat + Podcastit + Soittoäänet + QQ + TIM + WeChat + Nimi + Polku + Kuvan katselija + %1$,d/%2$,d + Tekstieditori + %1$s + *%1$s + Tallennettu + Haluatko varmasti ladata tiedoston uudelleen? Tallentamattomat muutokset häviävät. + Merkistökoodaus + Haluatko varmasti hylätä tiedoston tallentamattomat muutokset? + + FTP-palvelin + Tila + Käynnistetään… + Käynnissä + Pysäytetään… + Ei käynnistettynä + URL + Paikallinen IP-osoite on tuntematon + Kopioi URL + Kopioi salasana + Lisää pika-asetuksiin + “FTP-palvelin” on lisätty pika-asetuksiin + “FTP-palvelin” on jo lisättynä pika-asetuksiin + Virhe “FTP-palvelimen” lisäyksessä pika-asetuksiin + Asetukset + Anonyymi kirjautuminen + Käyttäjänimi + Salasana + Portti + Juurihakemisto + Salli kirjoittaminen + Asetukset + Käyttöliittymä + Kieli + Teemaväri + Eniten näytetty väri sovelluksessa + Material Design 3 + Yötila + + Seuraa järjestelmää + Pois päältä + Päältä + Aikaan perustuen + Virransäästöön perustue + + Musta yöteema + Tiedostolistan anmiaatioita + Näytä pitkä tiedostonimi + + Lyhennä alusta + Lyhennä keskeltä + Lyhennä lopusta + Marquee + + Toiminta + Oletuskansio + Standardihakemistot + Standardihakemistoja ei ole otettu käyttöön + Kuvakaappaukset + Kirjanmerkkikansiot + Ei kirjanmerkkikansioita + Root-käyttöoikeus + + %1$,d tiedostotoiminto on käynnissä ja root-käyttöoikeuden muuttaminen saattaa johtaa odottamattomaan virheeseen. Haluatko varmasti muuttaa asetusta nyt? + %1$,d tiedostotoimintoa on käynnissä ja root-käyttöoikeuden muuttaminen saattaa johtaa odottamattomaan virheeseen. Haluatko varmasti muuttaa asetusta nyt? + + + Vain normaalikäyttö + Automaattinen + Vain root-käyttö + + Pakkausten tiedostonimen koodaus + Avaa Android-pakkaus + + Asenna + Näytä sisältö + Kysy, mikä toiminto suoritetaan + + Näytä PDF-tiedostojen esikatselukuvat + Saattaa aiheuttaa sovelluksen epävakauden vanhemmilla Android-versioilla + Hae esikatselukuvat etätiedostoista + + Tietoa + Versio + Avaa GitHubissa + Lisenssit + Tietosuojaseloste + Tekijä + Hai Zhang + Seuraa GitHubissa + Seuraa Twitterissä + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 75dec792e..c05373a9e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -45,6 +45,7 @@ Montrer Passer Arrêter + Système par défaut Inconnu Voir @@ -55,6 +56,7 @@ %1$,d octet + %1$,d octets %1$,d octets @@ -71,94 +73,114 @@ Préparation de la compression de %1$,d fichier (%2$s) + Préparation de la compression de %1$,d fichiers (%2$s) Préparation de la compression de %1$,d fichiers (%2$s) Compresser “%1$s” dans “%2$s” Compresser %1$,d fichier dans “%2$s” + Compresser %1$,d fichiers dans “%2$s” Compresser %1$,d fichiers dans “%2$s” Préparation de la copie de %1$,d fichier (%2$s) + Préparation de la copie de %1$,d fichiers (%2$s) Préparation de la copie de %1$,d fichiers (%2$s) Copier “%1$s” dans “%2$s” Copier %1$,d fichier dans “%2$s” + Copier %1$,d fichiers dans “%2$s” Copier %1$,d fichiers dans “%2$s” Préparation de l\'extraction de %1$,d fichier (%2$s) + Préparation de l\'extraction de %1$,d fichiers (%2$s) Préparation de l\'extraction de %1$,d fichiers (%2$s) Extraire “%1$s” dans “%2$s” Extraire %1$,d fichier dans “%2$s” + Extraire %1$,d fichiers dans “%2$s” Extraire %1$,d fichiers dans “%2$s” Préparation du déplacement de %1$,d fichier (%2$s) + Préparation du déplacement de %1$,d fichiers (%2$s) Préparation du déplacement de %1$,d fichiers (%2$s) Déplacer “%1$s” dans “%2$s” Déplacer %1$,d fichier dans “%2$s” + Déplacer %1$,d fichiers dans “%2$s” Déplacer %1$,d fichiers dans “%2$s” %1$s / %2$s %1$,d / %2$,d Préparation de la suppression de %1$,d fichier + Préparation de la suppression de %1$,d fichiers Préparation de la suppression de %1$,d fichiers Supprimer “%1$s” Supprimer %1$,d fichier + Supprimer %1$,d fichiers Supprimer %1$,d fichiers Préparation du changement de propriétaire pour %1$,d fichier + Préparation du changement de propriétaire pour %1$,d fichiers Préparation du changement de propriétaire pour %1$,d fichiers Changer le propriétaire de “%1$s” Changer le propriétaire de %1$,d fichier + Changer le propriétaire de %1$,d fichiers Changer le propriétaire de %1$,d fichiers Préparation du changement de groupe pour %1$,d fichier + Préparation du changement de groupe pour %1$,d fichiers Préparation du changement de groupe pour %1$,d fichiers Changer le groupe de “%1$s” Changer le groupe de %1$,d fichier + Changer le groupe de %1$,d fichiers Changer le groupe de %1$,d fichiers Préparation du changement de mode pour %1$,d fichier + Préparation du changement de mode pour %1$,d fichiers Préparation du changement de mode pour %1$,d fichiers Changer le mode de “%1$s” Changer le mode de %1$,d fichier + Changer le mode de %1$,d fichiers Changer le mode de %1$,d fichiers Préparation du changement de contexte SElinux pour %1$,d fichier + Préparation du changement de contexte SElinux pour %1$,d fichiers Préparation du changement de contexte SElinux pour %1$,d fichiers Changement de contexte SELinux pour “%1$s” Changement du contexte SELinux pour %1$,d fichier + Changement du contexte SELinux pour %1$,d fichiers Changement du contexte SELinux pour %1$,d fichiers Préparation de la restauration du contexte SELinux pour %1$,d fichier + Préparation de la restauration du contexte SELinux pour %1$,d fichiers Préparation de la restauration du contexte SELinux pour %1$,d fichiers Restauration du context SELinux pour \"%1$s\" Restauration du contexte SELinux pour %1$,d fichier + Restauration du contexte SELinux pour %1$,d fichiers Restauration du contexte SELinux pour %1$,d fichiers %1$,d / %2$,d @@ -235,14 +257,17 @@ Supprimer le dossier “%1$s” et son contenu ? Supprimer %1$,d fichier? + Supprimer %1$,d fichiers? Supprimer %1$,d fichiers? Supprimer %1$,d dossier et son contenu ? + Supprimer %1$,d dossiers et leur contenu ? Supprimer %1$,d dossiers et leur contenu ? Supprimer %1$,d élément? + Supprimer %1$,d éléments? Supprimer %1$,d éléments? Créer une archive @@ -256,23 +281,30 @@ Fichiers Sélectionner un fichier + Sélectionner les fichiers Sélectionner les fichiers Sélectionner un dossier + Sélectionner les dossiers Sélectionner les dossiers %1$,d dossier + %1$,d dossiers %1$,d dossiers %1$,d fichier + %1$,d fichiers %1$,d fichiers ,\u0020 Le chemin ne peut pas être vide Chemin invalide + Afficher et trier + Liste + Grille Nom Type Taille @@ -287,9 +319,9 @@ Copier le chemin Ouvrir dans le terminal Ajouter un marque-page - Créer + Créer un raccourci Ouvrir dans une nouvelle fenêtre - %1$,d selectionné + %1$,d Selectionner “%1$s” Déplacer %1$,d Copier %1$,d @@ -317,6 +349,7 @@ Contenus %1$,d élément, pesant %2$s + %1$,d éléments, totalisant %2$s %1$,d éléments, totalisant %2$s Dernière Modification @@ -343,7 +376,7 @@ Lire - Ecrire + Écrire Recherche Spécial @@ -406,6 +439,7 @@ Aucune permission demandée %1$,d permission demandée + %1$,d permissions demandées %1$,d permissions demandées Signatures @@ -423,6 +457,10 @@ Modifier le stockage de l\'appareil Nom Chemin + Ajouter un raccourci DocumentsUI + Modifier le raccourci DocumentsUI + Entrez un URI + URI invalide Ajouter un support de stockage externe Modifier le support de stockage externe Nom @@ -492,7 +530,7 @@ Entrez un nom d\'hôte Nom d\'hôte invalide Port - Port non valide + Port invalide Chemin Peut être laissé vide Nom @@ -546,9 +584,13 @@ Arrêt… Pas démarré URL - Impossible de récupérer l’adresse IP locale + L\'adresse IP locale est inconnue Copier l\'URL Copier le mot de passe + Ajouter aux paramètre rapides + \"Serveur FTP\" a été ajouté aux paramètres rapides + \"Serveur FTP\" est déjà ajouté aux paramètres rapides + Erreur lors de l\'ajout de \"Serveur FTP\" aux paramètres rapides Configuration Connexion anonyme Nom d\'utilisateur @@ -556,18 +598,18 @@ Port Dossier root Autoriser l\'écriture - Réglages Interface + Langage Couleur du thème Couleur qui apparaît le plus souvent dans l’application Material Design 3 Mode nuit - Système de suivi - Off - On - Basé sur le temps + Suivre le système + Désactivé + Activé + Basé sur l\'heure Basé sur l’économiseur de batterie Mode nuit noire @@ -589,10 +631,11 @@ Mode d\'accès root Vous avez %1$,d opération de fichiers en cours d’exécution et changer maintenant le mode d’accès root peut entraîner une erreur inattendue. Êtes-vous sûr de vouloir changer cela maintenant ? + Vous avez %1$,d opérations de fichiers en cours d’exécution et changer maintenant le mode d’accès root peut entraîner une erreur inattendue. Êtes-vous sûr de vouloir changer cela maintenant ? Vous avez %1$,d opérations de fichiers en cours d’exécution et changer maintenant le mode d’accès root peut entraîner une erreur inattendue. Êtes-vous sûr de vouloir changer cela maintenant ? - Accès normal seulement + Accès normal uniquement Automatique Accès root uniquement diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 07ecebc2c..c51712367 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -26,6 +26,7 @@ Talán később Egyesítés Nincs + Navigációs sáv megnyitása Beillesztés Frissítés Újratöltés @@ -42,6 +43,7 @@ Megjelenítés Kihagyás Leállítás + Rendszer alapértelmezése Ismeretlen Nézet @@ -57,6 +59,8 @@ Az alkalmazásnak engedélyre van szüksége, hogy elérje a fájlokat. Kattintson az „Engedélyezésre” a következő rendszerüzeneten. Az alkalmazásnak engedélyre van szüksége, hogy elérje a fájlokat. Adja meg a „Tároló” engedélyt a rendszerbeállításokban. + Az alkalmazásnak hozzáférésre van szüksége az összes fájl kezeléséhez. Engedélyezze a hozzáférést a következő rendszer beállításban. + Műveletek a háttérben Műveletek elvégzése, míg az alkalmazás a háttérben van Fájlműveletek @@ -228,6 +232,18 @@ Már létezik egy fájl ezzel a névvel Törli a(z) „%1$s” fájlt? Törli a(z) „%1$s” mappát és a tartalmát? + + Törli a(z) %1$,d fájlt? + Töröl %1$,d fájlokat? + + + Töröl %1$,d mappát és azok tartalmát? + Töröl %1$,d mappát és azok tartalmát? + + + Töröl %1$,d elemet? + Töröl %1$,d elemet? + Archívum létrehozása .zip .tar.xz @@ -254,22 +270,28 @@ %1$,d fájl ,\u0020 + Az útvonal nem lehet üres + Érvénytelen útvonal + Megtekintés és rendezés + Lista + Rács Név Típus Méret - Utoljára módosítva + Legutóbb módosítva Növekvő Mappák elöl Csak ennél a mappánál Új ablak Ugrás fel + Ugrás ide: Rejtett fájlok megjelenítése Útvonal másolása Megnyitás terminálban Könyvjelző hozzáadása Parancsikon létrehozása Megnyitás új ablakban - %1$,d kiválasztva + %1$,d „%1$s” kiválasztása %1$,d áthelyezése %1$,d másolása @@ -299,7 +321,7 @@ %1$,d elem, mérete %2$s %1$,d elem, összesen %2$s - Utoljára módosítva + Legutóbb módosítva Szülőmappa Fájl archiválása Bejegyzés archiválása @@ -397,20 +419,54 @@ Nincs tároló Tároló hozzáadása Külső tároló + FTP-kiszolgáló SFTP-kiszolgáló SMB-kiszolgáló Tárolóeszköz szerkesztése Név Útvonal + DocumentsUI parancsikon hozzáadása + DocumentsUI parancsikon szerkesztése + Adjon meg URI-t + Érvénytelen URI Külső tároló hozzáadása Külső tároló szerkesztése Név URI Útvonal + FTP-kiszolgáló szerkesztése + FTP-kiszolgáló hozzáadása + Gépnév + Gépnév megadása + Érvénytelen gépnév + Port + Érvénytelen port + Útvonal + Üresen hagyható + Név + Gépnév használata + Protokoll + Hitelesítés + + Jelszó + Névtelen + + Felhasználónév + Felhasználónév megadása + Jelszó + Átviteli mód + + Aktív + Passzív + + Kódolás + Hozzáadás és csatlakozás + Hozzáadás SFTP-kiszolgáló szerkesztése SFTP-kiszolgáló hozzáadása Gépnév Adjon meg egy gépnevet + Érvénytelen gépnév Port Érvénytelen port Útvonal @@ -429,6 +485,9 @@ Fájl megnyitása Adjon meg egy privát kulcsot Érvénytelen privát kulcs + Privát kulcs jelszava + Üresen hagyható + Privát kulcs jelszava érvénytelen Kapcsolódás és hozzáadás Hozzáadása SMB-kiszolgálók keresése… @@ -437,6 +496,7 @@ SMB-kiszolgáló hozzáadása Gépnév Adjon meg egy gépnevet + Érvénytelen gépnév Port Érvénytelen port Útvonal @@ -474,6 +534,7 @@ Mappa könyvjelzőzése Név Útvonal + Archívum megjelenítő Képmegjelenítő %1$,d/%2$,d Szövegszerkesztő @@ -490,10 +551,14 @@ Fut Leállítás… Nem fut - URL - A helyi IP-cím nem kérhető le - URL másolása + Webcím + A helyi IP-cím ismeretlen + Webcím másolása Jelszó másolása + Hozzáadás a gyors beállításokhoz + Az „FTP-kiszolgáló” hozzá lett adva a gyors beállításokhoz + Az „FTP-kiszolgáló” már hozzá van adva a gyors beállításokhoz + Hiba történt az „FTP-kiszolgáló” gyors beállításokhoz történő hozzáadása során Beállítás Névtelen bejelentkezés Felhasználónév @@ -501,11 +566,12 @@ Port Gyökérmappa Írás engedélyezése - Beállítások Felület + Nyelv Téma színe A szín, amely a leggyakrabban megjelenik az alkalmazásban + Material Design 3 Éjszakai mód Rendszertéma követése @@ -537,7 +603,7 @@ Csak normál hozzáférés - Automatic + Automatikus Csak root hozzéférés Archívum fájlnévkódolása diff --git a/app/src/main/res/values-in/mime_types.xml b/app/src/main/res/values-in/mime_types.xml index 249a39cb3..5718e4e1b 100644 --- a/app/src/main/res/values-in/mime_types.xml +++ b/app/src/main/res/values-in/mime_types.xml @@ -17,8 +17,8 @@ Folder %1$s dokumen %1$s e-book - %1$s pesan surel - %1$s fon + %1$s pesan email + %1$s font %1$s berkas %1$s gambar Dokumen PDF @@ -30,6 +30,9 @@ Dokumen Word Presentasi PowerPoint Spreadsheet Excel + karakter perangkat + blok perangkat + pipa Tautan Soket Tautan (rusak) diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 8882b8960..2bb95026b 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -15,22 +15,22 @@ Bawaan Hapus Batal - Sunting + Edit Kosong (Kosong) - Galat + kesalahan Sembunyikan Pasang - Tetap menyunting + Tetap mengedit Memuat… Mungkin nanti Gabung - Nihil - Buka laci navigasi + Tidak ada + Buka drawer navigasi Tempel Segarkan Muat ulang - Buang + Hapus Ubah nama Ganti Setel ulang @@ -42,7 +42,7 @@ Bagikan Tampilkan Lewati - Setop + Hentikan Tidak diketahui Lihat @@ -187,9 +187,9 @@ Terjadi kesalahan saat menyimpan Telah terjadi kesalahan saat menyimpan “%1$s”.\n%2$s Telah terjadi kesalahan saat mengambil informasi tentang “%1$s”. - Mount ulang “%1$s” sebagai baca-tulis - Mount ulang “%1$s” sebagai baca-tulis… - “%1$s” telah dimount sebagai baca-tulis + Mount ulang “%1$s” sebagai read-write + Mount ulang “%1$s” sebagai read-write… + “%1$s” telah dimount sebagai read-write Terapkan untuk semua berkas Berkas ini adalah paket Android. Apakah Anda ingin memasangnya, atau melihat isinya? @@ -223,7 +223,7 @@ .zip .tar.xz .7z - Markah ditambahkan + Bookmark ditambahkan Berkas baru Folder baru @@ -249,17 +249,15 @@ Terakhir diubah Naik Folder di atas - Hanya untuk folder ini Jendela baru Ke atas Kunjungi Tampilkan berkas tersembunyi Salin jalur Buka di terminal - Tambahkan markah + Tambahkan bookmark Buat pintasan Buka di jendela baru - %1$,d dipilih Pilih “%1$s” Memindahkan %1$,d Menyalin %1$,d @@ -319,7 +317,7 @@ Atur UID Atur GID - Sticky bit + sedikit lengket Jangan tambah \"Eksekusi\" untuk seluruh rekursi berkas Konteks SELinux @@ -401,8 +399,8 @@ Nama hos Masukkan nama hos Nama hos tidak valid - Porta - Porta tidak valid + Port + Port tidak valid Jalur Boleh dikosongkan Nama @@ -429,8 +427,8 @@ Nama hos Masukkan nama hos Nama hos tidak valid - Porta - Porta tidak valid + Port + Port tidak valid Jalur Boleh dikosongkan Nama @@ -459,8 +457,8 @@ Nama hos Masukkan nama hos Nama hos tidak valid - Porta - Porta tidak valid + Port + Port tidak valid Jalur Boleh dikosongkan Nama @@ -505,7 +503,7 @@ Disimpan Apakah Anda yakin ingin memuat ulang? Perubahan yang belum disimpan pada dokumen ini akan hilang. Enkode - Apakah Anda yakin ingin membuang perubahan yang belum disimpan pada dokumen ini? + Apakah Anda yakin ingin menghapus perubahan yang belum disimpan pada dokumen ini? Server FTP Status @@ -514,17 +512,15 @@ Menghentikan… Tidak berjalan URL - Tidak bisa mengambil alamat IP lokal Salin URL Salin sandi Konfigurasi Masuk secara anonim Nama pengguna Sandi - Porta + Port Folder root Izinkan menyimpan - Pengaturan Antarmuka Warna tema @@ -538,7 +534,7 @@ Berdasarkan waktu Berdasarkan penghemat baterai - Mode malam + Mode super hitam Animasi daftar berkas Tampilkan nama berkas panjang @@ -552,8 +548,8 @@ Folder standar Tidak ada folder standar yang diaktifkan Screenshot - Folder markah - Tidak ada folder markah + Folder bookmark + Tidak ada folder bookmark Mode akses root Anda memiliki %1$,d proses berkas yang sedang berjalan dan mengubah mode akses root sekarang bisa mengakibatkan kesalahan yang tidak terduga. Apakah Anda yakin ingin mengubahnya sekarang? diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index a37a2abab..e58c903ed 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -45,6 +45,7 @@ Birta Sleppa Stöðva + Sjálfgefið í kerfinu Óþekkt Skoða @@ -273,6 +274,9 @@ ,\u0020 Slóð má ekki vera auð Ógild slóð + Skoðun og röðun + Listi + Reitir Nafn Tegund Stærð @@ -289,7 +293,7 @@ Bæta við bókamerki Útbúa flýtileið Opna í nýjum glugga - %1$,d valið + %1$,d Velja “%1$s” Færi %1$,d Afrita %1$,d @@ -423,6 +427,10 @@ Breyta geymslurými tækis Nafn Slóð + Bæta við DocumentsUI-flýtileið + Breyta DocumentsUI-flýtileið + Settu inn slóð + Ógild slóð Bæta við ytra geymslurými Breyta ytra geymslurými Nafn @@ -546,9 +554,13 @@ Stöðvast… Ekki ræst URL-slóð - Fæ ekki staðvært IP-vistfang + Staðvært IP-vistfang er ekki þekkt Afrita slóð Afrita lykilorð + Bæta í flýtistillingar + “FTP-netþjónn” hefur verið bætt í flýtistillingar + “FTP-netþjónn” hefur þegar verið bætt í flýtistillingar + Villa við að bæta “FTP-netþjónn” í flýtistillingar Uppsetning Nafnlaus innskráning Notandanafn @@ -556,9 +568,9 @@ Gátt Rótarmappa Leyfa skrifun - Stillingar Viðmót + Tungumál Litur þema Litur sem birtist oftast í forritinu Material-hönnun 3 diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0c460d15e..1408079c2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -43,6 +43,7 @@ Mostra Salta Ferma + Predefinito di sistema Sconosciuto Visualizza @@ -53,11 +54,12 @@ %1$,d byte + %1$,d bytes %1$,d bytes - L\'app richiede l\'autorizzazione di accedere ai file. Clicca \"CONSENTI\" nella prossima finestra. - L\'app richiede l\'autorizzazione di accedere ai file. Consenti l\'autorizzazione \"Archiviazione\" nelle impostazioni di sistema. + L\'app richiede l\'autorizzazione di accedere ai file. Clicca “CONSENTI” nella prossima finestra. + L\'app richiede l\'autorizzazione di accedere ai file. Consenti l\'autorizzazione “Archiviazione” nelle impostazioni di sistema. L\'app richiede l\'autorizzazione di gestire i file. Consenti l\'accesso nella prossima impostazione di sistema. Azioni in secondo piano @@ -69,94 +71,114 @@ Preparazione per comprimere %1$,d file (%2$s) + Preparazione per comprimere %1$,d file (%2$s) Preparazione per comprimere %1$,d file (%2$s) Compressione di “%1$s” in “%2$s” Compressione di %1$,d file in “%2$s” + Compressione di %1$,d file in “%2$s” Compressione di %1$,d file in “%2$s” Preparazione per copiare %1$,d file (%2$s) + Preparazione per copiare %1$,d file (%2$s) Preparazione per copiare %1$,d file (%2$s) Copia di “%1$s” in “%2$s” Copia di %1$,d file in “%2$s” + Copia di %1$,d file in “%2$s” Copia di %1$,d file in “%2$s” Preparazione per estrarre %1$,d file (%2$s) + Preparazione per estrarre %1$,d file (%2$s) Preparazione per estrarre %1$,d file (%2$s) Estrazione di “%1$s” in “%2$s” Estrazione di %1$,d file in “%2$s” + Estrazione di %1$,d file in “%2$s” Estrazione di %1$,d file in “%2$s” Preparazione per spostare %1$,d file (%2$s) + Preparazione per spostare %1$,d file (%2$s) Preparazione per spostare %1$,d file (%2$s) Spostamento di “%1$s” in “%2$s” Spostamento di %1$,d file in “%2$s” + Spostamento di %1$,d file in “%2$s” Spostamento di %1$,d file in “%2$s” %1$s / %2$s %1$,d / %2$,d Preparazione per eliminare %1$,d file + Preparazione per eliminare %1$,d file Preparazione per eliminare %1$,d file Eliminazione di “%1$s” Eliminazione di %1$,d file + Eliminazione di %1$,d file Eliminazione di %1$,d file Preparazione per cambiare il proprietario di %1$,d file + Preparazione per cambiare il proprietario di %1$,d file Preparazione per cambiare il proprietario di %1$,d file Modifica del proprietario di “%1$s” Modifica del proprietario di %1$,d file + Modifica del proprietario di %1$,d file Modifica del proprietario di %1$,d file Preparazione per cambiare il gruppo di %1$,d file + Preparazione per cambiare il gruppo di %1$,d file Preparazione per cambiare il gruppo di %1$,d file Modifica del gruppo di “%1$s” Modifica del gruppo di %1$,d file + Modifica del gruppo di %1$,d file Modifica del gruppo di %1$,d file Preparazione per cambiare i permessi di base di %1$,d file + Preparazione per cambiare i permessi di base di %1$,d file Preparazione per cambiare i permessi di base di %1$,d file Modifica dei permessi di base di “%1$s” Modifica dei permessi di base di %1$,d file + Modifica dei permessi di base di %1$,d file Modifica dei permessi di base di %1$,d file Preparazione per cambiare il contesto SELinux di %1$,d file + Preparazione per cambiare il contesto SELinux di %1$,d file Preparazione per cambiare il contesto SELinux di %1$,d file Modifica del contesto di SELinux di “%1$s” Modifica del contesto di SELinux di %1$,d file + Modifica del contesto di SELinux di %1$,d file Modifica del contesto di SELinux di %1$,d file Preparazione per ripristinare il contesto di SELinux di %1$,d file + Preparazione per ripristinare il contesto di SELinux di %1$,d file Preparazione per ripristinare il contesto di SELinux di %1$,d file Ripristino del contesto di SELinux di “%1$s” Ripristino del contesto di SELinux di %1$,d file + Ripristino del contesto di SELinux di %1$,d file Ripristino del contesto di SELinux di %1$,d file %1$,d / %2$,d @@ -233,14 +255,17 @@ Eliminare la cartella “%1$s” e il suo contenuto? Eliminare %1$,d file? + Eliminare %1$,d file? Eliminare %1$,d file? Eliminare %1$,d cartella e i suoi contenuti? + Eliminare %1$,d cartelle e i loro contenuti? Eliminare %1$,d cartelle e i loro contenuti? Eliminare %1$,d elemento? + Eliminare %1$,d elementi? Eliminare %1$,d elementi? Crea archivio @@ -254,23 +279,30 @@ File Seleziona un file + Seleziona più file Seleziona più file Seleziona una cartella + Seleziona cartelle Seleziona cartelle %1$,d cartella + %1$,d cartelle %1$,d cartelle %1$,d file + %1$,d file %1$,d file ,\u0020 Il percorso non può essere vuoto Percorso non valido + Visualizzazione e ordinamento + Lista + Griglia Nome Tipo Dimensione @@ -287,7 +319,7 @@ Aggiungi segnalibro Crea segnalibro Apri in una nuova finestra - %1$,d selezionato + %1$,d Seleziona “%1$s” Spostamento di %1$,d Copia di %1$,d @@ -315,6 +347,7 @@ Contenuto %1$,d elemento, con dimensione %2$s + %1$,d elementi, in totale %2$s %1$,d elementi, in totale %2$s Ultima modifica @@ -350,7 +383,7 @@ Imposta GID Sticky bit - Non aggiungere \"Esecuzione\" ai file contenuti + Non aggiungere “Esecuzione” ai file contenuti Contesto SELinux Cambia contesto SELinux Ripristina @@ -404,6 +437,7 @@ Nessuna autorizzazione richiesta %1$,d autorizzazione richiesta + %1$,d autorizzazioni richieste %1$,d autorizzazioni richieste Firme @@ -421,6 +455,10 @@ Modifica archiviazione dispositivo Nome Percorso + Aggiungi la scorciatoia DocumentsUI + Modifica la scorciatoia DocumentsUI + Inserisci un URI + URI non valido Aggiungi archiviazione esterna Modifica archiviazione esterna Nome @@ -534,7 +572,7 @@ *%1$s Salvato Sei sicuro di volere ricaricare? Le modifiche a questo documento non salvate verranno perse. - Codifica in corso + Codifica Sei sicuro di volere scartare le modifiche a questo documento non salvate? Server FTP @@ -544,9 +582,13 @@ Arresto… Spento URL - Impossibile rilevare l\'indirizzo IP locale + L\'indirizzo IP locale è sconosciuto Copia URL Copia password + Aggiungi alle impostazioni rapide + “Server FTP” è stato aggiunto alle Impostazioni rapide + “Server FTP” è già stato aggiunto alle Impostazioni rapide + Errore durante l\'aggiunta di “server FTP” alle impostazioni rapide Configurazione Accesso anonimo Nome utente @@ -554,9 +596,9 @@ Porta Cartella root Consenti scrittura - Impostazioni Interfaccia + Lingua Colore tema Colore che appare più spesso nell\'app Material Design 3 @@ -587,6 +629,7 @@ Modalità di accesso root È in corso %1$,d operazione su file e cambiare ora la modalità di accesso root potrebbe causare un errore. Sei sicuro di volere cambiarla ora? + Sono in corso %1$,d operazioni su file e cambiare ora la modalità di accesso root potrebbe causare un errore. Sei sicuro di volere cambiarla ora? Sono in corso %1$,d operazioni su file e cambiare ora la modalità di accesso root potrebbe causare un errore. Sei sicuro di volere cambiarla ora? diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 6fad5cb0b..b04206884 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -333,7 +333,6 @@ השתנה לאחרונה עולה תיקיות תחילה - לתיקייה זו בלבד חלון חדש עלה מעבר אל @@ -343,7 +342,6 @@ הוספת סימנייה יצירת קיצור דרך פתיחה בחלון חדש - %1$,d נבחרו בחירת “%1$s” מעביר את %1$,d מעתיק את %1$,d @@ -604,7 +602,6 @@ עוצר… לא התחיל כתובת - לא ניתן לאחזר כתובת IP מקומית העתקת כתובת העתקת סיסמה תצורה @@ -614,7 +611,6 @@ פתחה תיקיית שורש איפשור כתיבה - הגדרות ממשק צבע ערכת נושא diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f67d1a0be..49b3901c6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -259,7 +259,7 @@ ブックマークに追加 ショートカットを作成 新しい画面を開く - %1$,d 個選択済み + %1$,d “%1$s” を選択 %1$,d を移動 %1$,d をコピー @@ -514,7 +514,6 @@ 停止… 停止中 URL - ローカルIPアドレスが取得できません URL をコピー パスワードをコピー 環境設定 @@ -524,7 +523,6 @@ ポート Root フォルダ 書き込みを許可 - 設定 外観 テーマ色 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 7fdf0f4f9..aefc47248 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -43,6 +43,7 @@ 표시 건너 뛰기 중지 + 시스템 기본값 알 수 없음 보기 @@ -243,6 +244,9 @@ ,\u0020 경로를 입력하세요 올바르지 않은 경로 + 정렬 및 표시 방식 + 목록 + 격자 이름 유형 크기 @@ -259,7 +263,7 @@ 즐겨찾기에 추가 바로가기 만들기 새 창에서 열기 - %1$,d개 항목 선택함 + %1$,d “%1$s” 선택 %1$,d개 항목 이동 %1$,d개 항목 복사 @@ -321,7 +325,7 @@ Set GID Sticky bit - 내부 파일에 \"실행\" 권한 부여 안 함 + 내부 파일에 “실행” 권한 부여 안 함 SELinux 컨텍스트 SELinux 컨텍스트 변경 복원 @@ -391,6 +395,10 @@ 기기 저장소 편집 이름 경로 + DocumentsUI 바로가기 추가 + DocumentsUI 바로가기 편집 + URI를 입력하세요 + 올바르지 않은 URI 외부 저장소 추가 외부 저장소 편집 이름 @@ -514,9 +522,13 @@ 중지하는 중… 시작되지 않음 URL - 로컬 IP 주소를 찾을 수 없음 + 로컬 IP 주소 알 수 없음 URL 복사 비밀번호 복사 + 빠른 설정에 추가 + “FTP 서버”를 빠른 설정에 추가했습니다. + “FTP 서버”는 이미 빠른 설정에 추가되어 있습니다. + 빠른 설정에 “FTP 서버”를 추가하는 중 오류 설정 익명으로 로그인 허용 사용자 이름 @@ -524,9 +536,9 @@ 포트 루트 폴더 쓰기 허용 - 설정 인터페이스 + 언어 테마 색상 앱에서 가장 많이 보이는 색상 머티리얼 디자인 3 diff --git a/app/src/main/res/values-lt/mime_types.xml b/app/src/main/res/values-lt/mime_types.xml new file mode 100644 index 000000000..7c21c5cb1 --- /dev/null +++ b/app/src/main/res/values-lt/mime_types.xml @@ -0,0 +1,39 @@ + + + + + + Failas + Android paketas + %1$s archyvas + %1$s garso įrašas + %1$s kalendorius + %1$s sertifikatas + %1$s dokumentas + Elektroninė verslo kortelė + Aplankas + %1$s dokumentas + %1$s e. knyga + %1$s el. pašto žinutė + %1$s šriftas + %1$s failas + %1$s vaizdas + PDF dokumentas + %1$s pristatymas + %1$s skaičiuoklė + %1$s dokumentas + Paprasto teksto dokumentas + %1$s vaizdo įrašas + Word dokumentas + PowerPoint pristatymas + Excel skaičiuoklė + Simbolių įrenginys + Blokavimo įrenginys + Vamzdis + Nuoroda + Lizdas + Nuoroda (neveikianti) + diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml new file mode 100644 index 000000000..b6c06b1ff --- /dev/null +++ b/app/src/main/res/values-lt/strings.xml @@ -0,0 +1,693 @@ + + + + + + + Material Failai + + Uždaryti + Kopijuoti + Iškirpti + Numatytasis + Ištrinti + Atmesti + Redaguoti + Tuščia + (Tuščia) + Klaida + Slėpti + Įdiegti + Tęsti redagavimą + Kraunama… + Gal vėliau + Sujungti + Nėra + Atidaryti navigacijos dėklą + Įklijuoti + Atnaujinti + Įkelti iš naujo + Pašalinti + Pervadinti + Pakeisti + Nustatyti iš naujo + Bandyti dar kartą + Išsaugoti + Ieškoti + Pasirinkti + Pasirinkti viską + Bendrinti + Rodyti + Praleisti + Sustabdyti + Sistemos numatytasis + Nežinomas + Peržiūrėti + + + Nerasta jokių programų, galinčių atlikti šį veiksmą + Atidaryti nustatymus + Sukurtas trumpasis kelias + + + %1$,d baitas + %1$,d baitai + %1$,d baitų + %1$,d baitai + + + Programėlei reikia leidimo pasiekti failus. Būsimame sistemos dialogo lange spustelėk „LEISTI“. + Programėlei reikia leidimo pasiekti failus. Suteik saugyklos leidimą sistemos nustatymuose. + Programėlei reikia prieigos, kad būtų galima valdyti visus failus. Leisk prieigą būsimame sistemos nustatyme. + + Veiksmai, kol fone + Daryti veiksmus kai programėlė veikia fone + Failo operacijos + Rodyti ir valdyti failų operacijas + FTP serveris + Rodyti ir valdyti FTP serverį + + + Ruošiamasi suspausti %1$,d failą (%2$s) + Ruošiamasi suspausti %1$,d failus (%2$s) + Ruošiamasi suspausti %1$,d failų (%2$s) + Ruošiami suspausti %1$,d failus (%2$s) + + Suspaudžiama „%1$s“ į „%2$s“ + + Suspaudžiamas %1$,d failas į „%2$s“ + Suspaudžiami %1$,d failai į „%2$s“ + Suspaudžiami %1$,d failų į „%2$s“ + Suspaudžiami %1$,d failai į „%2$s“ + + + Ruošiamasi kopijuoti %1$,d failą (%2$s) + Ruošiamasi kopijuoti %1$,d failus (%2$s) + Ruošiamasi kopijuoti %1$,d failų (%2$s) + Ruošiami kopijuoti %1$,d failus (%2$s) + + Kopijuojama „%1$s“ į „%2$s“ + + Kopijuojamas %1$,d failas į „%2$s“ + Kopijuojami %1$,d failai į „%2$s“ + Kopijuojami %1$,d failų į „%2$s“ + Kopijuojami %1$,d failai į „%2$s“ + + + Ruošiamasi išskleisti %1$,d failą (%2$s) + Ruošiamasi išskleisti %1$,d failus (%2$s) + Ruošiamasi išskleisti %1$,d failų (%2$s) + Ruošiami išskleisti %1$,d failus (%2$s) + + Išskleidžiama „%1$s“ į „%2$s“ + + Išskleidžiamas %1$,d failas į „%2$s“ + Išskleidžiami %1$,d failai į „%2$s“ + Išskleidžiami %1$,d failų į „%2$s“ + Išskleidžiami %1$,d failai į „%2$s“ + + + Ruošiamasi perkelti %1$,d failą (%2$s) + Ruošiamasi perkelti %1$,d failus (%2$s) + Ruošiamasi perkelti %1$,d failų (%2$s) + Ruošiami perkelti %1$,d failus (%2$s) + + Perkeliama „%1$s“ į „%2$s“ + + Perkeliamas %1$,d failas į „%2$s“ + Perkeliami %1$,d failai į „%2$s“ + Perkeliami %1$,d failų į „%2$s“ + Perkeliami %1$,d failai į „%2$s“ + + %1$s / %2$s + %1$,d / %2$,d + + Ruošiamasi ištrinti %1$,d failą + Ruošiamasi ištrinti %1$,d failus + Ruošiamasi ištrinti %1$,d failų + Ruošiami ištrinti %1$,d failus + + Ištrinama „%1$s“ + + Ištrinamas %1$,d failas + Ištrinami %1$,d failai + Ištrinami %1$,d failų + Ištrinami %1$,d failai + + + Ruošiamasi keisti savininką %1$,d failui + Ruošiamasi keisti savininką %1$,d failams + Ruošiamasi keisti savininką %1$,d failų + Ruošiami keisti savininką %1$,d failams + + Keičiamas „%1$s“ savininkas + + Keičiamas %1$,d failo savininkas + Keičiamas %1$,d failų savininkas + Keičiamas %1$,d failų savininkas + Keičiamas %1$,d failų savininkas + + + Ruošiamasi keisti %1$,d failo grupę + Ruošiamasi keisti %1$,d failam grupę + Ruošiamasi keisti %1$,d failų grupę + Ruošiami keisti %1$,d failų grupę + + Keičiama „%1$s“ grupė + + Keičiama %1$,d failo grupė + Keičiama %1$,d failų grupė + Keičiama %1$,d failų grupė + Keičiama %1$,d failų grupė + + + Ruošiamasi keisti %1$,d failo režimą + Ruošiamasi keisti %1$,d failų režimą + Ruošiamasi keisti %1$,d failų režimą + Ruošiami keisti %1$,d failų režimą + + Keičiamas „%1$s“ režimas + + Keičiamas režimas %1$,d failui + Keičiamas režimas %1$,d failams + Keičiamas režimas %1$,d failų + Keičiamas režimas %1$,d failams + + + Ruošiami keisti SELinux kontekstą %1$,d failui + Ruošiami keisti SELinux kontekstą %1$,d failams + Ruošiami keisti SELinux kontekstą %1$,d failų + Ruošiami keisti SELinux kontekstą %1$,d failus + + Keičiamas SELinux kontekstas „%1$s“ + + Keičiamas SELinux kontekstas %1$,d failui + Keičiamas SELinux kontekstas %1$,d failams + Keičiamas SELinux kontekstas %1$,d failų + Keičiamas SELinux kontekstas %1$,d failus + + + Ruošiamasi atkurti SELinux kontekstą %1$,d failui + Ruošiamasi atkurti SELinux kontekstą %1$,d failams + Ruošiamasi atkurti SELinux kontekstą %1$,d failų + Ruošiamasi atkurti SELinux kontekstą %1$,d failus + + Atkuriamas SELinux kontekstas „%1$s“ + + Atkuriamas SELinux kontekstas %1$,d failui + SELinux konteksto atkūrimas %1$,d failams + Atkuriamas SELinux kontekstas %1$,d failų + SELinux konteksto atkūrimas %1$,d failus + + %1$,d / %2$,d + Rašoma “%1$s” + Klaida kuriant + Įvyko klaida sukuriant “%1$s”.\n%2$s + Negalima nukopijuoti aplanko į jį patį + Negalima išskleisti aplanko į jį patį + Negalima perkelti aplanko į jį patį + Paskirties aplankas yra šaltinio aplanke. + Negalima nukopijuoti failo į jį patį + Negalima išskleisti failo į jį patį + Negalima perkelti failo į jį patį + Šaltinio failas būtų perrašytas paskirties vietos. + Pakeisti failą \"%1$s\"? + Kitas tokio paties pavadinimo failas jau yra “%1$s”.\nPakeitus jį, bus perrašytas jo turinys. + Originalus failas + Pakeisti su + Sujungti aplanką \"%1$s\"? + Prieš pakeičiant bet kokius aplanke esančius failus, kurie prieštarauja kopijuojamiems failams, bus paprašyta patvirtinimo. + Prieš pakeičiant bet kokius aplanke esančius failus, kurie prieštarauja išskleistiems failams, bus paprašyta patvirtinimo. + Prieš pakeičiant bet kokius aplanke esančius failus, kurie prieštarauja perkeliamiems failams, bus paprašyta patvirtinimo. + Originalus aplankas + Sujungti su + Pasirinkite naują paskirties vietos pavadinimą + Naujas pavadinimas + Įvyko klaida suspaudžiant “%1$s” + Įvyko klaida suspaudžiant failą į “%1$s”.\n%2$s + Klaida kopijuojant “%1$s” + Įvyko klaida kopijuojant failą į “%1$s”.\n%2$s + Klaida išskleidžiant “%1$s” + Įvyko klaida išskleidžiant failą į “%1$s”.\n%2$s + Įvyko klaida perkeliant “%1$s” + Įvyko klaida perkeliant failą į “%1$s”.\n%2$s + Įvyko klaida trinant + Įvyko klaida trinant “%1$s”.\n%2$s + Įvyko klaida pervadinant “%1$s” + Įvyko klaida pervadinant failą į “%1$s”.\n%2$s + Įvyko klaida keičiant “%1$s” savininką + Įvyko klaida keičiant savininką į “%1$s”.\n%2$s + Įvyko klaida keičiant “%1$s” grupę + Įvyko klaida keičiant grupę į “%1$s”.\n%2$s + Įvyko klaida keičiant “%1$s” režimą + Įvyko klaida keičiant režimą į “%1$s”.\n%2$s + Įvyko klaida keičiant “%1$s” SELinux kontekstą + Įvyko klaida keičiant SELinux kontekstą į “%1$s”.\n%2$s + Įvyko klaida atkuriant SELinux kontekstą + Įvyko klaida atkuriant SELinux kontekstą „%1$s“.\n%2$s + Klaida rašant + Įvyko klaida rašant “%1$s”.\n%2$s + Įvyko klaida gaunant informaciją apie “%1$s”. + Permontuoti „%1$s“ kaip skaitymo ir rašymo + Permontavimas „%1$s“ kaip skaitymo ir rašymo… + „%1$s“ buvo permontuotas kaip skaitymo ir rašymo + Taikyti šį veiksmą visiems failams + + Šis failas yra Android paketas. Ar norite jį įdiegti arba peržiūrėti jo turinį? + Atidaryti kaip… + Atidaryti \"%1$s\" kaip + Tekstas + Nuotrauka + Garso įrašas + Vaizdo įrašas + Aplankas + Kiti + “%1$s” yra paruoštas įdiegimui + Bakstelėkite, kad įdiegti + \"%1$s\" yra paruoštas atidarymui + Bakstelėkite, kad atidaryti + Failo pavadinimas negali būti tuščias + Netinkamas failo pavadinimas + Failas šiuo pavadinimu jau egzistuoja + Ištrinti \"%1$s\"? + Ištrinti aplanką \"%1$s\" ir jo turinį? + + Ištrinti %1$,d failą? + Ištrinti %1$,d failus? + Ištrinti %1$,d failų? + Ištrinti %1$,d failus? + + + Ištrinti %1$,d aplanką ir jo turinį? + Ištrinti %1$,d aplankus ir jų turinį? + Ištrinti %1$,d aplankų ir jų turinį? + Ištrinti %1$,d aplankus ir jų turinį? + + + Ištrinti %1$,d įrašą? + Ištrinti %1$,d įrašus? + Ištrinti %1$,d įrašų? + Ištrinti %1$,d įrašus? + + Sukurti archyvą + .zip + .tar.xz + .7z + Pridėta žymė + Naujas failas + Naujas aplankas + + Failai + + Pasirinkti failą + Pasirinkti failus + Pasirinkti failus + Pasirinkti failus + + + Pasirinkti aplanką + Pasirinkti aplankus + Pasirinkti aplankus + Pasirinkti aplankus + + + %1$,d aplankas + %1$,d aplankai + %1$,d aplankų + %1$,d aplankai + + + %1$,d failas + %1$,d failai + %1$,d failų + %1$,d failai + + ,\u0020 + Kelias negali būti tuščias + Netinkamas kelias + Peržiūra ir rūšiavimas + Sąrašas + Tinklelis + Pavadinimas + Tipas + Dydis + Paskutinį kartą keista + Didėjantis + Aplankai pirmiausia + Tik šiam aplankui + Naujas langas + Eiti į viršų + Eiti į + Rodyti paslėptus failus + Kopijuoti kelią + Atidaryti terminale + Pridėti žymę + Sukurti spartųjį kelią + Atidaryti naujame lange + %1$,d + Pasirinkti \"%1$s\" + Perkeliama %1$,d + Kopijuojama %1$,d + Išskleidžiama %1$,d + Išskleisti čia + Nėra failų + Failas + Aplankas + + \u0020\u0020\u0020\u0020 + Atidaryti su + Išskleisti + Suspausti + Savybės + + “%1$s” savybės + Pagrindiniai + Pavadinimas + Tipas + %1$s (%2$s) + Nuoroda į %1$s (%2$s) + Nuorodos tikslas + Dydis + %1$s (%2$s) + Turinys + + %1$,d įrašas, kurio dydis %2$s + %1$,d įrašai, viso %2$s + %1$,d įrašų, viso %2$s + %1$,d įrašai, viso %2$s + + Paskutinį kartą keista + Pirminis aplankas + Archyvo failas + Archyvo įrašas + Laisvos vietos + Leidimai + Savininkas + Grupė + %1$s (%2$d) + Keisti savininką + Keisti grupę + Įvesti pavadinimą ar ID + Sistema + Režimas + %1$s (%2$04o) + Keisti režimą + Kiti + + Skaityti + Rašyti + Paleisti + + + Skaityti + Rašyti + Ieškoti + + Specialūs + + Nustatyti UID + Nustatyti GID + Lipnus bitas + + Nepridėti „Vykdyti“ pridedamiems failams + SELinux kontekstas + Keisti SELinux kontekstą + Atkurti + Taikyti pridedamiems failams + Nuotrauka + Matmenys + %1$d \u00d7 %2$d + Padaryta + Koordinatės + %1$.3f, %2$.3f + Vietovė + Aukštis + %1$,.3f m + Kamera + %1$s %2$s + Diafragma + Sklendės greitis + Fokusavimo nuotolis + %1$.2f mm + ISO atitikmuo + ISO %1$d + Programinė įranga + Aprašymas + Kūrėjas + Autorinės teisės + Vaizdo įrašas + Pavadinimas + Atlikėjas + Albumas + Albumo atlikėjas + Kompozitorius + Diskas + Takelis + Metai + Žanras + Trukmė + Bitų sparta + %1$d kbps + Mėginių ėmimo dažnis + %1$d Hz + Vaizdo įrašas + APK + Pavadinimas + Paketo pavadinimas + Versija + %1$s (%2$d) + Minimali sistemos versija + Tikslinė sistemos versija + %1$s (%2$s, %3$d) + Leidimai + Jokių leidimų neprašoma + + %1$,d prašomas leidimas + %1$,d prašomi leidimai + %1$,d prašomų leidimų + %1$,d prašomi leidimai + + Parašai + Nėra tinkamų parašų + Seni parašai + + Root + Saugykla + Nėra saugyklos + Pridėti saugyklą + Išorinė saugykla + FTP serveris + SFTP serveris + SMB serveris + Redaguoti įrenginio saugylką + Pavadinimas + Kelias + Pridėti DocumentsUI nuorodą + Redaguoti DocumentsUI nuorodą + Įvesk URI + Neteisingas URI + Pridėti išorinę saugyklą + Redaguoti išorinę saugyklą + Pavadinimas + URI + Kelias + Redaguoti FTP serverį + Pridėti FTP serverį + Serverio vardas + Įveskite serverio vardą + Netinkamas serverio vardas + Prievadas + Netinkamas prievadas + Kelias + Gali būti paliktas tuščias + Pavadinimas + Naudoti serverio vardą + Protokolas + Autentifikacija + + Slaptažodis + Anonimas + + Naudotojo vardas + Įvesti naudotojo vardą + Slaotažodis + Režimas + + Aktyvus + Pasyvus + + Kodavimas + Prisijungti ir pridėti + Pridėti + Redaguoti SFTP serverį + Pridėti SFTP serverį + Serverio vardas + Įveskite serverio vardą + Netinkamas serverio vardas + Prievadas + Netinkamas prievadas + Kelias + Gali būti paliktas tuščias + Pavadinimas + Naudoti serverio vardą + Autentifikacija + + Slaptažodis + Viešas raktas + + Naudotojo vardas + Įvesti naudotojo vardą + Slaptažodis + Privatus raktas + Atidaryti failą + Įvesti privatų raktą + Netinkamas privatus raktas + Privataus rakto slaptažodis + Gali būti paliktas tuščias + Netinkamas privataus rakto slaptažodis + Prisijungti ir pridėti + Pridėti + Ieškoma SMB serverių… + Pridėti rankiniu būdu + Redaguoti SMB serverį + Pridėti SMB serverį + Serverio vardas + Įveskite serverio vardą + Netinkamas serverio vardas + Prievadas + Netinkamas prievadas + Kelias + Gali būti paliktas tuščias + Pavadinimas + Naudoti serverio vardą + Autentifikacija + + Slaptažodis + Svečias + Anonimas + + Naudotojo vardas + Įvesti naudotojo vardą + Slaptažodis + Domenas + Prisijungti ir pridėti + Pridėti + + %1$s laisva iš %2$s + Pridėti saugyklą… + Aliarmai + DCIM + Dokumentai + Atsisiuntimai + Filmai + Muzika + Pranešimai + Paveikslėliai + Transliacijos + Skambučių melodijos + QQ + TIM + WeChat + Pažymėtas aplankas + Pavadinimas + Kelias + Archyvų žiūryklė + Nuotraukų žiūryklė + %1$,d/%2$,d + Teksto redaktorius + %1$s + *%1$s + Išsaugota + Ar tikrai norite įkelti iš naujo? Neišsaugoti šio dokumento pakeitimai bus prarasti. + Kodavimas + Ar tikrai norite atmesti neišsaugotus šio dokumento pakeitimus? + + FTP serveris + Būsena + Startuojamas… + Veikia + Sustabdomas… + Išjungtas + URL + Vietinis IP adresas nežinomas + Kopijuoti URL + Kopijuoti slaptažodį + Pridėti prie greitųjų nustatymų + „FTP serveris“ buvo pridėtas prie greitųjų nustatymų + „FTP serveris“ jau pridėtas prie greitųjų nustatymų + Klaida pridedant „FTP serveris“ prie greitųjų nustatymų + Konfigūracija + Anonimiškas prisijungimas + Naudotojo vardas + Slaptažodis + Prievadas + Root aplankas + Leisti rašyti + Nustatymai + Sąsaja + Kalba + Temos spalva + Dažniausiai programėlėje pasitaikanti spalva + Material Design 3 + Nakties režimas + + Sekti sistemą + Išj. + Įj. + Pagal laiką + Pagal akumuliatoriaus taupymą + + Juodos nakties režimas + Failų sąrašo animacija + Rodyti ilgą failo pavadinimą + + Elipsuok pradžią + Elipsuok vidurį + Elipsuok pabaigą + Stendas + + Elgsena + Numatytas aplankas + Standartiniai aplankai + Neįjungti jokie standartiniai aplankai + Ekrano nuotraukos + Pažymėti aplankai + Nėra pažymėtų aplankų + Root prieigos režimas + + Vykdoma %1$,d failų operacija, todėl dabar pakeitus root prieigos režimą gali įvykti netikėta klaida. Ar tikrai norite jį keisti dabar? + Vykdomos %1$,d failų operacijos, todėl dabar pakeitus root prieigos režimą gali įvykti netikėta klaida. Ar tikrai norite jį keisti dabar? + Vykdomos %1$,d failų operacijos, todėl dabar pakeitus root prieigos režimą gali įvykti netikėta klaida. Ar tikrai norite jį keisti dabar? + Vykdomos %1$,d failų operacijos, todėl dabar pakeitus root prieigos režimą gali įvykti netikėta klaida. Ar tikrai norite jį keisti dabar? + + + Tik įprasta prieiga + Automatinis + Tik root prieiga + + Archyvo failo pavadinimo kodavimas + Atidaryti Android paketą + + Įdiegti + Peržiūrėti turinį + Klausti ką daryti + + Rodyti miniatiūras PDF failams + Gali padaryti programėlę nestabilią senesnėse Android versijose + Rodyti nuotolinių failų miniatiūras + + Apie + Versija + Peržiūrėti GitHub svetainėje + Licencijos + Privatumo Politika + Autorius + Hai Zhang + Sekti GitHub svetainėje + Sekti Twitter svetainėje + diff --git a/app/src/main/res/values-nb/mime_types.xml b/app/src/main/res/values-nb/mime_types.xml new file mode 100644 index 000000000..2a241166f --- /dev/null +++ b/app/src/main/res/values-nb/mime_types.xml @@ -0,0 +1,39 @@ + + + + + + Fil + Android-pakke + %1$s-arkiv + %1$s-lyd + %1$s-kalender + %1$s-sertifikat + %1$s-dokument + Elektronisk visittkort + Mappe + %1$s-dokument + %1$s e-bok + %1$s e-postmelding + %1$s-skrifttype + %1$s-fil + %1$s-bilde + PDF-dokument + %1$s-presentasjon + %1$s-regneark + %1$s-dokument + Dokument med ren tekst + %1$s-video + Word-dokument + PowerPoint-presentasjon + Excel-regneark + Tegnenhet + Blokkenhet + Rør + Lenke + Kontakt + Lenke (brutt) + diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml new file mode 100644 index 000000000..f7f49b87b --- /dev/null +++ b/app/src/main/res/values-nb/strings.xml @@ -0,0 +1,629 @@ + + + + + + + Material Filer + + Lukk + Kopier + Klipp ut + Standard + Slett + Forkast + Rediger + Tom + (Tom) + Feil + Skjul + Installer + Fortsett å redigere + Laster inn … + Kanskje senere + Slå sammen + Ingen + Åpne uttrekksmenyen + Lim inn + Last inn på nytt + Last inn på nytt + Fjern + Gi nytt navn + Erstatt + Tilbakestill + Prøv på nytt + Lagre + Søk + Velg + Velg alle + Del + Vis + Hopp over + Stopp + Systemstandard + Ukjent + Se innholdet + + + Finner ingen apper som kan utføre denne handlingen + Åpne innstillingene + Snarveien er opprettet + + + %1$,d byte + %1$,d byte + + + Appen trenger tilgang til filene. Trykk på «TILLAT» i den kommende systemdialogen. + Appen trenger tilgang til filene. Gi «Lagring»-tillatelsen i systeminnstillingene. + Appen trenger tillatelse til å administrere alle filer. Gi tillatelse i de kommende systeminnstillingene. + + Handlinger i bakgrunnen + Utfør handlinger mens appen er i bakgrunnen + Filoperasjoner + Se og kontrollér filoperasjoner + FTP-tjener + Se og kontrollér FTP-tjeneren + + + Forbereder komprimering av %1$,d fil (%2$s) + Forbereder komprimering av %1$,d filer (%2$s) + + Komprimerer «%1$s» til «%2$s» + + Komprimerer %1$,d fil til «%2$s» + Komprimerer %1$,d filer til «%2$s» + + + Forbereder kopiering av %1$,d fil (%2$s) + Forbereder kopiering av %1$,d filer (%2$s) + + Kopierer «%1$s» til «%2$s» + + Kopierer %1$,d fil til «%2$s» + Kopierer %1$,d filer til «%2$s» + + + Forbereder utpakkingen av %1$,d fil (%2$s) + Forbereder utpakking av %1$,d filer (%2$s) + + Pakker ut «%1$s» til «%2$s» + + Pakker ut %1$,d fil til «%2$s» + Pakker ut %1$,d filer til «%2$s» + + + Forbereder flytting av %1$,d fil (%2$s) + Forbereder flytting av %1$,d filer (%2$s) + + Flytter «%1$s» til «%2$s» + + Flytter %1$,d fil til «%2$s» + Flytter %1$,d filer til «%2$s» + + %1$s / %2$s + %1$,d / %2$,d + + Forbereder sletting av %1$,d fil + Forbereder sletting av %1$,d filer + + Sletter «%1$s» + + Sletter %1$,d fil + Sletter %1$,d filer + + + Forbereder endring av eier for %1$,d fil + Forbereder endring av eier for %1$,d filer + + Endrer eier for «%1$s» + + Endrer eier for %1$,d fil + Endrer eier for %1$,d filer + + + Forbereder endring av gruppe for %1$,d fil + Forbereder endring av gruppe for %1$,d filer + + Endrer gruppe for «%1$s» + + Endrer gruppe for %1$,d fil + Enrer gruppe for %1$,d filer + + + Forbereder endring av modus for %1$,d fil + Forbereder endring av modus for %1$,d filer + + Endrer modus for «%1$s» + + Endrer modus for %1$,d fil + Endrer modus for %1$,d filer + + + Forbereder endring av SElinux-kontekst for %1$,d fil + Forbereder endring av SElinux-kontekst for %1$,d filer + + Endrer SELinux-kontekst for «%1$s» + + Endrer SELinux-kontekst for %1$,d fil + Endrer SELinux-kontekst for %1$,d filer + + + Forbereder tilbakestilling av SELinux-kontekst for %1$,d fil + Forbereder gjenoppretting av SELinux-kontekst for %1$,d filer + + Gjenoppretter SELinux-kontekst for «%1$s» + + Gjenoppretter SELinux-kontekst for %1$,d fil + Gjenoppretter SELinux-kontekst for %1$,d filer + + %1$,d / %2$,d + Skriver til «%1$s» + Feil under oppretting + Det oppsto en feil under opprettingen av «%1$s».\n%2$s + Kan ikke kopiere en mappe inn i seg selv + Kan ikke pakke ut en mappe inn i seg selv + Kan ikke flytte en mappe inn i seg selv + Målmappen er inne i kildemappen. + Kan ikke kopiere en fil over seg selv + Kan ikke pakke ut en fil over seg selv + Kan ikke flytte en fil over seg selv + Kildefilen ville blitt overskrevet av målfilen. + Vil du erstatte «%1$s»-filen? + En annen fil med det samme navnet finnes allerede i «%1$s».\nÅ erstatte den vil overskrive innholdet til filen. + Opprinnelig fil + Erstatt med + Vil du slå sammen mappen «%1$s»? + Sammenslåing vil be om bekreftelse før noen av filene i mappen som er i konflikt blir erstattet med filene som blir kopiert. + Sammenslåing vil be om bekreftelse før noen av filene i mappen som er i konflikt blir erstattet med filene som blir pakket ut. + Sammenslåing vil be om bekreftelse før noen av filene i mappen som er i konflikt blir erstattet med filene som blir flyttet. + Opprinnelig mappe + Slå sammen med + Velg et nytt navn for målet + Nytt navn + Feil under komprimering av «%1$s» + Det oppsto en feil under komprimering av filen inn i «%1$s».\n%2$s + Feil under kopiering av «%1$s» + Det oppsto en feil under kopiering av filen inn i «%1$s».\n%2$s + Feil under utpakking av «%1$s» + Det oppsto en feil under utpakking av filen inn i «%1$s».\n%2$s + Feil under flytting av «%1$s» + Det oppsto en feil under flytting av filen inn i «%1$s».\n%2$s + Feil under sletting + Det oppsto en feil under sletting av «%1$s».\n%2$s + Feil under endring av navn for «%1$s» + Det oppsto en feil under endring av filnavnet til «%1$s».\n%2$s + Feil under endring av eier for «%1$s» + Det oppsto en feil under endring av eier til «%1$s».\n%2$s + Feil under endring av gruppe for «%1$s» + Det oppsto en feil under endring av gruppe til «%1$s».\n%2$s + Feil under endring av modus for «%1$s» + Det oppsto en feil under endring av modus til «%1$s».\n%2$s + Feil under endring av SELinux-kontekst for «%1$s» + Det oppsto en feil under endring av SELinux-kontekst til «%1$s».\n%2$s + Feil under gjenoppretting av SELinux-kontekst + Det oppsto en feil under gjenoppretting av SELinux-kontekst for «%1$s».\n%2$s + Feil under skriving + Det oppsto en feil under skriving til «%1$s».\n%2$s + Det oppsto en feil under henting av informasjon om «%1$s». + Koble til «%1$s» på nytt med lese- og skrivetilgang + Kobler til «%1$s» på nytt med lese- og skrivetilgang … + «%1$s» er koblet til på nytt med lese- og skrivetilgang + Bruk denne handlingen på alle filene + + Denne filen er en Android-pakke. Vil du installere den eller se innholdet i den? + Åpne som … + Åpne «%1$s» som + Tekst + Bilde + Lyd + Video + Mappe + Annet + «%1$s» er klar til installering + Trykk for å installere + «%1$s» er klar til åpning + Trykk for å åpne + Filnavnet kan ikke være tomt + Ugyldig filnavn + Det finnes allerede en fil med dette navnet + Vil du slette «%1$s»? + Vil du slette «%1$s»-mappen og innholdet i den? + + Vil du slette %1$,d fil? + Vil du slette %1$,d filer? + + + Vil du slette %1$,d mappe og innholdet i den? + Vil du slette %1$,d mapper og innholdet i dem? + + + Vil du slette %1$,d element? + Vil du slette %1$,d elementer? + + Opprett arkiv + .zip + .tar.xz + .7z + Bokmerke er lagt til + Ny fil + Ny mappe + + Filer + + Velg en fil + Velg filer + + + Velg en mappe + Velg mapper + + + %1$,d mappe + %1$,d mapper + + + %1$,d mappe + %1$,d mapper + + ,\u0020 + Banen kan ikke være tom + Ugyldig bane + Se og sortér + Liste + Rutenett + Navn + Type + Størrelse + Sist endret + Stigende + Mapper først + Kun for denne mappen + Nytt vindu + Gå opp + Gå til + Vis skjulte filer + Kopier bane + Åpne i terminalen + Legg til bokmerke + Opprett snarvei + Åpne i et nytt vindu + %1$,d + Velg «%1$s» + Flytter %1$,d + Kopierer %1$,d + Pakker ut %1$,d + Pakk ut her + Ingen filer + Fil + Mappe + + \u0020\u0020\u0020\u0020 + Åpne med + Pakk ut + Komprimer + Egenskaper + + Egenskaper for «%1$s» + Generelt + Navn + Type + %1$s (%2$s) + Lenke til %1$s (%2$s) + Lenkemål + Størrelse + %1$s (%2$s) + Innhold + + %1$,d fil, med størrelse %2$s + %1$,d filer, totalt %2$s + + Sist endret + Plassering + Arkivfil + Arkivoppføring + Ledig plass + Tillatelser + Eier + Gruppe + %1$s (%2$d) + Endre eier + Endre gruppe + Skriv inn et navn eller ID + System + Modus + %1$s (%2$04o) + Endre modus + Andre + + Lese + Skrive + Kjøre + + + Lese + Skrive + Søk + + Spesielt + + Angi UID + Angi GID + Fast + + Ikke legg til «Kjør» på underfilene + SELinux-kontekst + Endre SELinux-kontekst + Gjenopprett + Bruk på alle underfilene + Bilde + Dimensjoner + %1$d \u00d7 %2$d + Tatt + Koordinater + %1$.3f, %2$.3f + Posisjon + Høyde over havet + %1$,.3f m + Kamera + %1$s %2$s + Blender + Lukkerhastighet + Brennvidde + %1$.2f mm + Tilsvarende ISO + ISO %1$d + Programvare + Beskrivelse + Opprettet av + Opphavsrett + Lyd + Tittel + Artist + Album + Albumartist + Komponist + Disk + Spor + År + Sjanger + Varighet + Bithastighet + %1$d kbit/s + Samplefrekvens + %1$d Hz + Video + APK + Navn + Pakkenavn + Versjon + %1$s (%2$d) + Minimum systemversjon + Målsystemversjon + %1$s (%2$s, %3$d) + Tillatelser + Ingen tillatelser er forespurt + + %1$,d tillatelse er forespurt + %1$,d tillatelser er forespurt + + Signaturer + Ingen gyldige signaturer + Gamle signaturer + + Rot + Lagring + Ingen lagring + Legg til lagring + Ekstern lagring + FTP-tjener + SFTP-tjener + SMB-tjener + Rediger lagring på enheten + Navn + Bane + Legg til DocumentsUI-snarvei + Rediger DocumentsUI-snarvei + Skriv inn en URI + Ugyldig URI + Legg til ekstern lagring + Rediger ekstern lagring + Navn + URI + Bane + Rediger FTP-tjener + Legg til FTP-tjener + Vertsnavn + Skriv inn et vertsnavn + Ugyldig vertsnavn + Port + Ugyldig port + Bane + Kan være tomt + Navn + Bruk vertsnavn + Protokoll + Autentisering + + Passord + Anonym + + Brukernavn + Skriv inn et brukernavn + Passord + Modus + + Aktiv + Passiv + + Koding + Koble til og legg til + Legg til + Rediger SFTP-tjener + Legg til SFTP-tjener + Vertsnavn + Skriv inn vertsnavn + Ugyldig vertsnavn + Port + Ugyldig port + Bane + Kan være tomt + Navn + Bruk vertsnavn + Autentisering + + Passord + Offentlig nøkkel + + Brukernavn + Skriv inn et brukernavn + Passord + Privat nøkkel + Åpne fil + Skriv inn en privat nøkkel + Ugyldig privat nøkkel + Passord for privat nøkkel + Kan være tomt + Ugyldig passord for privat nøkkel + Koble til og legg til + Legg til + Søker etter SMB-tjenere … + Legg til manuelt + Rediger SMB-tjener + Legg til SMB-tjener + Vertsnavn + Skriv inn et vertsnavn + Ugyldig vertsnavn + Port + Ugyldig port + Bane + Kan være tomt + Navn + Bruk vertsnavn + Autentisering + + Passord + Gjest + Anonym + + Brukernavn + Skriv inn et brukernavn + Passord + Domene + Koble til og legg til + Legg til + + %1$s av %2$s ledig + Legg til lagring … + Alarmer + DCIM + Dokumenter + Nedlastinger + Filmer + Musikk + Varsler + Bilder + Podcaster + Ringetoner + QQ + TIM + WeChat + Bokmerket mappe + Navn + Bane + Arkivleser + Bildefremviser + %1$,d/%2$,d + Tekstredigering + %1$s + *%1$s + Lagret + Er du sikker på at du vil laste inn på nytt? Ulagrede endringer i dette dokumentet vil gå tapt. + Tegnkoding + Er du sikker på at du vil forkaste de ulagrede endringene i dette dokumentet? + + FTP-tjener + Status + Starter … + Kjører + Stopper … + Ikke startet + URL + Lokal IP-adresse er ukjent + Kopier URL + Kopier passord + Legg til i hurtiginnstillingene + «FTP-tjener» er lagt til i hurtiginnstillingene + «FTP-tjener» er allerede lagt til i hurtiginnstillingene + Feil under tilføying av «FTP-tjener» til hurtiginnstillingene + Konfigurasjon + Anonym innlogging + Brukernavn + Passord + Port + Rotmappen + Tillat skriving + Innstillinger + Grensesnitt + Språk + Temafarge + Den mest brukte fargen i appen + Material Design 3 + Nattmodus + + Følg systemet + Av + + Basert på tid + Basert på batterisparing + + Svart nattmodus + Animer fillisten + Vis lange filnavn + + Ellipse i starten + Ellipse i midten + Ellipse på slutten + Rulletekst + + Atferd + Startmappe + Standardmapper + Ingen standardmapper er aktivert + Skjermdumper + Bokmerkede mapper + Ingen bokmerkede mapper + Tilgang som rot + + Du kjører %1$,d filoperasjon; å endre tilgangsmodusen nå kan føre til uventede feil. Er du sikker på at du vil endre den nå? + Du kjører %1$,d filoperasjoner; å endre tilgangsmodusen nå kan føre til uventede feil. Er du sikker på at du vil endre den nå? + + + Bare vanlig tilgang + Automatisk + Bare tilgang som rot + + Tegnkoding for filnavn til arkiv + Åpne Android-pakke + + Installer + Se innholdet + Spør + + Vis miniatyrbilde for PDF-filer + Kan gjøre appen ustabil på eldre Android-versjoner + Les eksterne filer for miniatyrbilder + + Om appen + Versjon + Se på GitHub + Lisenser + Personvernregler + Utvikler + Hai Zhang + Følg på GitHub + Følg på Twitter + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 5ce20f5c2..9bfd377c3 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -43,6 +43,7 @@ Tonen Overslaan Stoppen + Systeemstandaard Onbekend Bekijken @@ -271,6 +272,9 @@ ,\u0020 Voer een locatie in Ongeldige locatie + Weergave en sortering + Lijst + Rooster Naam Soort Grootte @@ -287,7 +291,7 @@ Bladwijzer toevoegen Snelkoppeling maken Openen in nieuw venster - %1$,d geselecteerd + %1$,d ‘%1$s’ selecteren Bezig met verplaatsen van %1$,d Bezig met kopiëren van %1$,d @@ -421,6 +425,10 @@ Apparaatopslag bewerken Naam Locatie + Documentsnelkoppeling maken + Documentsnelkoppeling bewerken + Voer een uri in + Ongeldige uri Externe opslag toevoegen Externe opslag bewerken Naam @@ -544,9 +552,13 @@ Bezig met stoppen… Niet gestart URL - Kan lokaal ip-adres niet ophalen + Het lokale ip-adres is onbekend URL kopiëren Wachtwoord kopiëren + Toevoegen aan snelle instellingen + De ftp-server is toegevoegd aan de snelle instellingen + De ftp-server is al toegevoegd aan de snelle instellingen + De ftp-server kan niet worden toegevoegd aan de snelle instellingen Instellingen Anoniem inloggen Gebruikersnaam @@ -554,9 +566,9 @@ Poort Hoofdmap Wegschrijven toestaan - Instellingen Uiterlijk + Taal Themakleur De kleur die het vaakst wordt gebruikt in de app Material Design 3 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 427eaa2d8..2b9dd198e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -38,11 +38,12 @@ Zapisz Szukaj Wybierz - Wybierz wszystkie + Wybierz wszystko Udostępnij Pokaż Pomiń Zatrzymaj + Domyślne systemu Nieznany Podgląd @@ -217,7 +218,7 @@ Inny plik o tej nazwie już istnieje w „%1$s”.\nZastąpienie go spowoduje nadpisanie jego zawartości. Oryginalny plik Zastąpić z - Scalić katalog „%1$s\"? + Scalić katalog „%1$s”? Operacja scalania poprosi o potwierdzenie przed zastąpieniem jakichkolwiek plików w katalogu, które powodują konflikt z kopiowanymi plikami. Operacja scalania poprosi o potwierdzenie przed zastąpieniem jakichkolwiek plików w katalogu, które powodują konflikt z rozpakowywanymi plikami. Operacja scalania poprosi o potwierdzenie przed zastąpieniem jakichkolwiek plików w katalogu, które powodują konflikt z przenoszonymi plikami. @@ -227,7 +228,7 @@ Nowa nazwa Błąd podczas kompresowania „%1$s” Wystąpił błąd podczas kompresowania pliku do „%1$s”.\n%2$s - Błąd podczas kopiowania „%1$s\" + Błąd podczas kopiowania „%1$s” Wystąpił błąd podczas kopiowania pliku do „%1$s”.\n%2$s Błąd podczas rozpakowywania „%1$s” Wystąpił błąd podczas rozpakowywania pliku do „%1$s”.\n%2$s @@ -327,13 +328,16 @@ ,\u0020 Ścieżka nie może być pusta Nieprawidłowa ścieżka + Przeglądaj i sortuj + Lista + Siatka Nazwa Typ Rozmiar Ostatnia modyfikacja Rosnąco Najpierw katalogi - Tylko dla tego katalogu + Tylko dla tego folderu Nowe okno Idź w górę Idź do @@ -343,7 +347,7 @@ Dodaj zakładkę Utwórz skrót Otwórz w nowym oknie - %1$,d wybrano + %1$,d Wybierz „%1$s” Przenoszenie %1$,d Kopiowanie %1$,d @@ -481,6 +485,10 @@ Edytuj pamięć urządzenia Nazwa Ścieżka + Dodaj skrót DocumentsUI + Edytuj skrót DocumentsUI + Podaj URI + Nieprawidłowy URI Dodaj nośnik zewnętrzny Edytuj nośnik zewnętrzny Nazwa @@ -604,9 +612,13 @@ Zatrzymywanie… Nie uruchomiony Adres URL - Niemożna pobrać lokalnego adresu IP + Lokalny adres IP jest nieznany Skopiuj adres URL Skopiuj hasło + Dodaj do szybkich ustawień + „Serwer FTP” został dodany do szybkich ustawień + „Serwer FTP” został już dodany do szybkich ustawień + Wystąpił błąd podczas dodawania kafelka „Serwer FTP” do szybkich ustawień Konfiguracja Logowanie anonimowe Nazwa użytkownika @@ -614,9 +626,9 @@ Port Katalog główny Zezwól na zapis - Ustawienia Interfejs + Język Kolor motywu Kolor pojawiający się najczęściej w aplikacji Material Design 3 @@ -630,7 +642,7 @@ Czarny tryb nocny Animacja listy plików - Wyświetl długą nazwę pliku + Wyświetlanie długiej nazwy pliku Wielokropek na początku Wielokropek w środku diff --git a/app/src/main/res/values-pt-rBR/mime_types.xml b/app/src/main/res/values-pt-rBR/mime_types.xml index 18acea5e4..347e9fa5f 100644 --- a/app/src/main/res/values-pt-rBR/mime_types.xml +++ b/app/src/main/res/values-pt-rBR/mime_types.xml @@ -25,9 +25,9 @@ %1$s apresentação %1$s planilha %1$s documento - Documento de texto puro + Documento de texto simples %1$s vídeo - Documento no Word + Documento do Word Apresentação do PowerPoint Planilha do Excel Dispositivo de caractere diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 9175cc3da..cdee498ea 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -28,6 +28,7 @@ Tente mais tarde Mesclar Nenhum + Abrir menu de navegação Colar Atualizar Recarregar @@ -54,11 +55,14 @@ %1$,d byte + %1$,d bytes %1$,d bytes O aplicativo precisa de permissão para acessar os arquivos. Clique em \"PERMITIR\" na próxima caixa de diálogo do sistema. O aplicativo precisa de permissão para acessar os arquivos. Por favor, conceda a permissão de \"Armazenamento\" nas configurações do sistema. + O aplicativo precisa de permissão para acessar os arquivos. Clique em \"PERMITIR\" na próxima caixa de diálogo do sistema. + Ações em segundo plano Realize ações enquanto o aplicativo estiver em segundo plano Operações de arquivo @@ -68,94 +72,114 @@ Preparando para compactar %1$,d arquivo (%2$s) + Preparando para compactar %1$,d arquivos (%2$s) Preparando para compactar %1$,d arquivos (%2$s) Compactando “%1$s” em “%2$s” Compactando %1$,d arquivo em “%2$s” + Compactando %1$,d arquivos em “%2$s” Compactando %1$,d arquivos em “%2$s” Preparando para copiar %1$,d arquivo (%2$s) + Preparando para copiar %1$,d arquivos (%2$s) Preparando para copiar %1$,d arquivos (%2$s) Copiando “%1$s” para “%2$s” Copiando %1$,d arquivo para “%2$s” + Copiando %1$,d arquivos para “%2$s” Copiando %1$,d arquivos para “%2$s” Preparando para extrair %1$,d arquivo (%2$s) + Preparando para extrair %1$,d arquivos (%2$s) Preparando para extrair %1$,d arquivos (%2$s) - Extraindo \"%1$s\" para \"%2$s\" + Extraindo “%1$s” para “%2$s” Extraindo %1$,d arquivo para \"%2$s\" - Extraindo %1$,d arquivos para \"%2$s\" + Extraindo %1$,d arquivos para \"%2$s\" + Extraindo %1$,d arquivos para “%2$s” Preparando para mover %1$,d arquivo (%2$s) + Preparando para mover %1$,d arquivos (%2$s) Preparando para mover %1$,d arquivos (%2$s) - Movendo \"%1$s\" para \"%2$s\" + Movendo “%1$s” para “%2$s” Movendo %1$,d arquivo para \"%2$s\" - Movendo %1$,d arquivos para \"%2$s\" + Movendo %1$,d arquivos para \"%2$s\" + Movendo %1$,d arquivos para “%2$s” %1$s / %2$s %1$,d / %2$,d Preparando para excluir %1$,d arquivo + Preparando para excluir %1$,d arquivos Preparando para excluir %1$,d arquivos - Excluindo \"%1$s\" + Excluindo “%1$s” Excluindo %1$,d arquivo + Excluindo %1$,d arquivos Excluindo %1$,d arquivos Preparando para mudar proprietário de %1$,d arquivo + Preparando para alterar proprietário de %1$,d arquivos Preparando para alterar proprietário de %1$,d arquivos - Alterando proprietário de \"%1$s\" + Alterando proprietário de “%1$s” Alterando proprietário de %1$,d arquivo + Alterando proprietário de %1$,d arquivos Alterando proprietário de %1$,d arquivos Preparando para alterar grupo de %1$,d arquivo + Preparando para alterar grupo de %1$,d arquivos Preparando para alterar grupo de %1$,d arquivos Alterando grupo de \"%1$s\" Alterando grupo de %1$,d arquivo + Alterando grupo de %1$,d arquivos Alterando grupo de %1$,d arquivos Preparando para alterar modo de %1$,d arquivo + Preparando para alterar modo de %1$,d arquivos Preparando para alterar modo de %1$,d arquivos Alterando modo de \"%1$s\" Alterando modo de %1$,d arquivo + Alterando modo de %1$,d arquivos Alterando modo de %1$,d arquivos Preparando para alterar o contexto do SELinux de %1$,d arquivo + Preparando para alterar o contexto do SELinux de %1$,d arquivos Preparando para alterar o contexto do SELinux de %1$,d arquivos Alterando o contexto do SELinux de \"%1$s\" Alterando o contexto do SELinux de %1$,d arquivo + Alterando o contexto do SELinux de %1$,d arquivos Alterando o contexto do SELinux de %1$,d arquivos Preparando para restaurar o contexto do SELinux de %1$,d arquivo + Preparando para restaurar o contexto do SELinux de %1$,d arquivos Preparando para restaurar o contexto do SELinux de %1$,d arquivos Restaurando o contexto do SELinux de \"%1$s\" Restaurando o contexto do SELinux de %1$,d arquivo + Restaurando o contexto do SELinux de %1$,d arquivos Restaurando o contexto do SELinux de %1$,d arquivos %1$,d / %2$,d @@ -230,6 +254,21 @@ Um arquivo com esse nome já existe Excluir \"%1$s\"? Excluir a pasta \"%1$s\" e seu conteúdo? + + Excluir %1$,d arquivo? + Excluir %1$,d arquivos? + Excluir %1$,d arquivos? + + + Excluir %1$,d pasta e seu conteúdo? + Excluir %1$,d pastas e seus conteúdos? + Excluir %1$,d pastas e seus conteúdos? + + + Excluir %1$,d item? + Excluir %1$,d item? + Excluir %1$,d itens? + Criar arquivo .zip .tar.xz @@ -241,37 +280,42 @@ Arquivos Selecione um arquivo + Selecionar arquivos Selecionar arquivos Selecione uma pasta + Selecionar pastas Selecionar pastas %1$,d pasta + %1$,d pastas %1$,d pastas %1$,d arquivo + %1$,d arquivos %1$,d arquivos ,\u0020 + Caminho não pode estar vazio + Caminho inválido Nome Tipo Tamanho Última modificação Crescente Pastas primeiro - Somente para esta pasta Nova janela Subir + Ir para Mostrar arquivos ocultos - Caminho de cópia + Copiar caminho Abrir no terminal Adicionar aos favoritos Criar atalho Abrir em uma nova janela - %1$,d selecionado Selecionar “%1$s” Movendo %1$,d Copiando %1$,d @@ -299,6 +343,7 @@ Conteúdo %1$,d item, com peso de %2$s + %1$,d itens, totalizando %2$s %1$,d itens, totalizando %2$s Última Modificação @@ -388,6 +433,7 @@ Nenhuma permissão solicitada %1$,d permissões solicitada + %1$,d permissões solicitadas %1$,d permissões solicitadas Assinaturas @@ -399,6 +445,7 @@ Nenhum armazenamento Adicionar armazenamento Armazenamento externo + Servidor FTP Servidor SFTP Servidor SMB Editar armazenamento do dispositivo @@ -409,10 +456,39 @@ Nome URI Caminho + Editar servidor FTP + Adicionar servidor FTP + Hostname + Insira o hostname + Hostname inválido + Porta + Porta inválida + Caminho + Pode ser deixado vazio + Nome + Usar hostname + Protocolo + Autenticação + + Senha + Anônimo + + Nome do usuário + Insira o nome do usuário + Senha + Modo + + Ativo + Passivo + + Codificação + Conectar e adicionar + Adicionar Editar servidor SFTP Adicionar servidor SFTP Hostname Insira o hostname + Hostname inválido Porta Porta inválida Caminho @@ -424,13 +500,16 @@ Senha Chave pública - Nome de usuário - Insira o nome de usuário + Nome do usuário + Insira o nome do usuário Senha Chave privada Abrir arquivo Insira a chave privada Chave privada inválida + Senha chave privada + Pode ser deixado vazio + Senha de chave privada inválida Conectar e adicionar Adicionar Procurando servidores SMB… @@ -439,6 +518,7 @@ Adicionar servidor SMB Hostname Insira o hostname + Hostname inválido Porta Porta inválida Caminho @@ -451,8 +531,8 @@ Convidado Anônimo - Usuário - Insira o nome de usuário + Nome do usuário + Insira o nome do usuário Senha Domínio Conecte e adicione @@ -476,6 +556,7 @@ Pasta de favoritos Nome Caminho + Visualizador de arquivos Visualizador de imagens %1$,d/%2$,d Editor de texto @@ -493,21 +574,20 @@ Parando… Não iniciado URL - Não é possível recuperar o endereço IP local Copiar URL Copiar senha Configuração Login anônimo - Nome de usuário + Nome do usuário Senha Porta Pasta raiz Permitir escrita - Configurações Interface Cor do tema Cor que aparece com mais frequência no aplicativo + Material Design 3 Modo noturno Seguir o sistema @@ -535,11 +615,12 @@ Modo de acesso root Você tem %1$,d operação de arquivo sendo executada e alterar o modo de acesso root agora pode resultar em erros inesperados. Tem certeza de que deseja alterar agora? + Você tem %1$,d operações de arquivos sendo executadas e alterar o modo de acesso root agora pode resultar em erros inesperados. Tem certeza de que deseja alterar agora? Você tem %1$,d operações de arquivos sendo executadas e alterar o modo de acesso root agora pode resultar em erros inesperados. Tem certeza de que deseja alterar agora? Apenas acesso normal - Automatic + Automático Apenas acesso root Codificação do nome do arquivo diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 205276643..029f5df99 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -17,6 +17,7 @@ Padrão Eliminar Descartar + Editar Vazio (Vazio) Erro @@ -27,6 +28,7 @@ Talvez mais tarde Juntar Nenhum + Abrir menu de navegação Colar Recarregar Recarregar @@ -43,6 +45,7 @@ Mostrar Ignorar Parar + Definições do sistema Desconhecido Ver @@ -53,11 +56,14 @@ %1$,d byte + %1$,d bytes %1$,d bytes A aplicação precisa da permissão para aceder aos ficheiros. Clique em “PERMITIR” na próxima caixa de diálogo. A aplicação precisa da permissão para aceder aos ficheiros. Por favor, conceda a permissão de “Armazenamento” nas definições de sistema. + A aplicação necessita do acesso para gerir os ficheiros/pastas. Conceda esse acesso na definição seguinte. + Ações em segundo plano Executar ações se a aplicação estiver em segundo plano Operações de ficheiro @@ -67,94 +73,114 @@ A preparar para comprimir %1$,d ficheiro (%2$s) + A preparar para comprimir %1$,d ficheiros (%2$s) A preparar para comprimir %1$,d ficheiros (%2$s) A comprimir “%1$s” para “%2$s” A comprimir %1$,d ficheiro para “%2$s” + A comprimir %1$,d ficheiros para “%2$s” A comprimir %1$,d ficheiros para “%2$s” A preparar para copiar %1$,d ficheiro (%2$s) + A preparar para copiar %1$,d ficheiros (%2$s) A preparar para copiar %1$,d ficheiros (%2$s) A copiar “%1$s” para “%2$s” A copiar %1$,d ficheiro para “%2$s” + A copiar %1$,d ficheiros para “%2$s” A copiar %1$,d ficheiros para “%2$s” A preparar para extrair %1$,d ficheiro (%2$s) + A preparar para extrair %1$,d ficheiros (%2$s) A preparar para extrair %1$,d ficheiros (%2$s) A extrair “%1$s” para “%2$s” A extrair %1$,d ficheiro para “%2$s” + A extrair %1$,d ficheiros para “%2$s” A extrair %1$,d ficheiros para “%2$s” A preparar para mover %1$,d ficheiro (%2$s) + A preparar para mover %1$,d ficheiros (%2$s) A preparar para mover %1$,d ficheiros (%2$s) A mover “%1$s” para “%2$s” A mover %1$,d ficheiro para “%2$s” + A mover %1$,d ficheiros para “%2$s” A mover %1$,d ficheiros para “%2$s” %1$s / %2$s %1$,d / %2$,d A preparar para eliminar %1$,d ficheiro + A preparar para eliminar %1$,d ficheiros A preparar para eliminar %1$,d ficheiros A eliminar “%1$s” A eliminar %1$,d ficheiro + A eliminar %1$,d ficheiros A eliminar %1$,d ficheiros A preparar para alterar o proprietário de %1$,d ficheiro + A preparar para alterar o proprietário de %1$,d ficheiros A preparar para alterar o proprietário de %1$,d ficheiros A alterar proprietário de “%1$s” A alterar proprietário de %1$,d ficheiro + A alterar proprietário de %1$,d ficheiros A alterar proprietário de %1$,d ficheiros A preparar parar alterar o grupo de %1$,d ficheiro + A preparar parar alterar o grupo de %1$,d ficheiros A preparar parar alterar o grupo de %1$,d ficheiros A alterar o grupo de “%1$s” A alterar o grupo de %1$,d ficheiro + A alterar o grupo de %1$,d ficheiros A alterar o grupo de %1$,d ficheiros A preparar para alterar o modo de %1$,d ficheiro + A preparar para alterar o modo de %1$,d ficheiros A preparar para alterar o modo de %1$,d ficheiros A alterar modo de “%1$s” A alterar o modo de %1$,d ficheiro + A alterar o modo de %1$,d ficheiros A alterar o modo de %1$,d ficheiros A preparar para alterar o contexto SELinux de %1$,d ficheiro + A preparar para alterar o contexto SELinux de %1$,d ficheiros A preparar para alterar o contexto SELinux de %1$,d ficheiros A alterar contexto SELinux de “%1$s” A alterar contexto SELinux de %1$,d ficheiro + A alterar contexto SELinux de %1$,d ficheiros A alterar contexto SELinux de %1$,d ficheiros A preparar para repor o contexto SELinux de %1$,d ficheiro + A preparar para repor o contexto SELinux de %1$,d ficheiros A preparar para repor o contexto SELinux de %1$,d ficheiros A repor contexto SELinux de “%1$s” A repor contexto SELinux de %1$,d ficheiro + A repor contexto SELinux de %1$,d ficheiros A repor contexto SELinux de %1$,d ficheiros %1$,d / %2$,d @@ -229,6 +255,21 @@ Já existe um ficheiro com este nome Eliminar “%1$s”? Eliminar a pasta “%1$s” e todo o seu conteúdo? + + Eliminar %1$,d ficheiro? + Eliminar %1$,d ficheiros? + Eliminar %1$,d ficheiros? + + + Eliminar %1$,d pasta e todo o seu conteúdo? + Eliminar %1$,d pastas e todo o seu conteúdo? + Eliminar %1$,d pastas e todo o seu conteúdo? + + + Eliminar %1$,d item? + Eliminar %1$,d itens? + Eliminar %1$,d itens? + Criar arquivo .zip .tar.xz @@ -240,21 +281,30 @@ Ficheiros Selecionar ficheiro + Selecionar ficheiros Selecionar ficheiros Selecionar pasta + Selecionar pastas Selecionar pastas %1$,d pasta + %1$,d pastas %1$,d pastas %1$,d ficheiro + %1$,d ficheiros %1$,d ficheiros ,\u0020 + Caminho não pode estar vazio + Caminho inválido + Vista e ordenação + Lista + Grelha Nome Tipo Tamanho @@ -264,13 +314,14 @@ Apenas para esta pasta Nova janela Subir + Ir para Mostrar ficheiros ocultos Copiar caminho Abrir no terminal Adicionar marcador Criar atalho Abrir em nova janela - %1$,d selecionado + %1$,d “%1$s” selecionado A mover %1$,d A copiar %1$,d @@ -298,6 +349,7 @@ Conteúdo %1$,d item, tamanho: %2$s + %1$,d itens, tamanho: %2$s %1$,d itens, tamanho: %2$s Última modificação @@ -387,9 +439,11 @@ Não necessita de permissões Necessita de %1$,d permissão + Necessita de %1$,d permissões Necessita de %1$,d permissões Assinaturas + Não há assinaturas válidas Assinaturas antigas Root @@ -397,23 +451,88 @@ Nenhum armazenamento Adicionar armazenamento Armazenamento externo + Servidor FTP + Servidor SFTP Servidor SMB Editar armazenamento Nome Caminho + Adicionar atalho \'DocumentsUI\' + Editar atalho \'DocumentsUI\' + Indique um URI + URI inválido Adicionar armazenamento externo Editar armazenamento externo Nome URI Caminho + Editar servidor FTP + Adicionar servidor FTP + Nome do servidor + Indique um nome + Nome inválido + Porta + Porta inválida + Caminho + Pode ser deixado vazio + Nome + Utilizar nome do servidor + Protocolo + Autenticação + + Palavra-passe + Anónimo + + Nome de utilizador + Indique um nome de utilizador + Palavra-passe + Modo + + Ativo + Passivo + + Codificação + Conectar e adicionar + Adicionar + Editar servidor SFTP + Adicionar servidor SFTP + Nome do servidor + Indique um nome + Nome inválido + Porta + Porta inválida + Caminho + Pode ser deixado vazio + Nome + Utilizar nome do servidor + Autenticação + + Palavra-passe + Chave pública + + Nome de utilizador + Indique um nome de utilizador + Palavra-passe + Chave privada + Abrir ficheiro + Indique a chave privada + Chave privada inválida + Palavra-passe da chave privada + Pode ser deixado vazio + Palavra-passe da chave privada inválida + Conectar e adicionar + Adicionar A procurar servidores SMB… Adicionar manualmente Editar servidor SMB Adicionar servidor SMB Nome do servidor Introduza um nome + Nome inválido Porta Porta inválida + Caminho + Pode ser deixado vazio Nome Utilizar nome do servidor Autenticação @@ -447,6 +566,7 @@ Pasta de marcadores Nome Caminho + Visualizador de arquivos Visualizador de imagens %1$,d/%2$,d Editor de texto @@ -454,6 +574,7 @@ *%1$s Guardado Tem a certeza de que deseja recarregar? As alterações não guardadas serão perdidas. + Codificação Tem certeza de que deseja descartar as alterações efetuadas a este documento? Servidor FTP @@ -463,9 +584,13 @@ A parar… Não iniciado URL - Não foi possível obter o endereço IP local + Endereço local desconhecido Copiar URL Copiar palavra-passe + Adicionar a Definições rápidas + “Servidor FTP” adicionado a Definições rápidas + “Servidor FTP” já existe em Definições rápidas + Erro ao adicionar “Servidor FTP” a Definições rápidas Configuração Sessão anónima Nome de utilizador @@ -473,11 +598,12 @@ Porta Pasta root Permitir escrita - Definições Interface + Idioma Cor do tema A cor que aparece com mais frequência na aplicação + Material Design 3 Modo noturno Cor do sistema @@ -505,11 +631,12 @@ Modo de acesso root Tem %1$,d operação de ficheiro em execução e se alterar o modo de acesso root agora podem ocorrer erros inesperados. Tem a certeza de que deseja alterar agora? + Tem %1$,d operações de ficheiro em execução e se alterar o modo de acesso root agora podem ocorrer erros inesperados. Tem a certeza de que deseja alterar agora? Tem %1$,d operações de ficheiro em execução e se alterar o modo de acesso root agora podem ocorrer erros inesperados. Tem a certeza de que deseja alterar agora? Apenas acesso normal - Automatic + Automático Apenas acesso root Codificação para o nome do arquivo @@ -519,6 +646,8 @@ Ver conteúdo Perguntar o que fazer + Mostrar miniaturas para ficheiros PDF + Pode tornar-se instável nas versões mais antigas do Android Ler ficheiros remotos das miniaturas Acerca diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index f92b715e8..acd1acec1 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -43,6 +43,7 @@ Arată Omite Oprire + Prestabilit de sistem Necunoscut Vizualizează @@ -299,6 +300,9 @@ ,\u0020 Calea nu poate fi liberă Cale invalidă + Vizualizare și sortare + Listă + Tabel Nume Tip Mărime @@ -315,7 +319,7 @@ Adaugă marcaj Creează scurtătură Deschide într-o fereastră nouă - %1$,d selectat + %1$,d Selectat “%1$s” Se mută %1$,d Se copiază %1$,d @@ -451,6 +455,10 @@ Editează stocarea dispozitivului Nume Cale + Adaugă o scurtătură pentru DocumentUI + Editează scurtătura pentru DocumentsUI + Introdu un URI + URI invalid Adaugă stocare externă Editează stocarea externă Nume @@ -574,9 +582,13 @@ Se oprește… Nu este pornit URL - Nu se poate prelua adresa IP locală + Adresa IP locală nu este cunoscută Copiază URL Copiază parola + Adaugă la Setările Rapide + \"Server FTP\" a fost adăugat la Setări Rapide + \"Server FTP\" este deja adăugat la Setări Rapide + A apărut o eroare la adăugarea \"Serverului FTP\" la Setările Rapide Configurare Autentificare anonimă Nume utilizator @@ -584,9 +596,9 @@ Port Dosar rădăcină Permite scrierea - Setări Interfață + Limbă Culoarea temei Culoarea ce apare cel mai frecvent în aplicație Material Design 3 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c8bcb6102..f967bcbc6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -51,7 +51,7 @@ Приложение не найдено для обработки данного действия Открыть настройки - Шорткат создан + Ярлык создан %1$,d байт @@ -335,7 +335,6 @@ Последнее изменение По возрастанию Папки первые - Только для этой папки Новое окно Вверх Перейти к @@ -343,9 +342,8 @@ Скопировать путь Открыть в терминале Добавить закладку - Создать шорткат + Создать ярлык Открыть в новом окне - %1$,d выбрано Выбрать “%1$s” Перемещение %1$,d Копирование %1$,d @@ -606,7 +604,6 @@ Остановка… Не запущен URL - Не удаётся получить локальный IP-адрес Скопировать URL Скопировать пароль Конфигурация @@ -616,7 +613,6 @@ Порт Корневой каталог Разрешить запись - Настройки Интерфейс Цвет темы @@ -676,6 +672,6 @@ Политика конфиденциальности Автор Hai Zhang - Фолловить на GitHub - Фолловить в Twitter + Подписаться на GitHub + Подписаться в Twitter diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c89126c8f..413361ec5 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -262,7 +262,6 @@ Son düzenleme Artan Önce klasörler - Sadece bu klasör için Yeni pencere Yukarı git Gizli dosyaları göster @@ -271,7 +270,6 @@ Yer işareti ekle Kısayol oluştur Yeni pencerede aç - %1$,d seçildi “%1$s”`i seç %1$,d taşınıyor %1$,d kopyalanıyor @@ -525,7 +523,6 @@ Durduruluyor… Başlatılmadı URL - Yerel IP adresi alınamıyor URL\'yi kopyala Şifreyi kopyala Yapılandırma @@ -535,7 +532,6 @@ Port Kök klasör Yazmaya izin ver - Ayarlar Arayüz Tema rengi diff --git a/app/src/main/res/values-uk/mime_types.xml b/app/src/main/res/values-uk/mime_types.xml new file mode 100644 index 000000000..59c6945ac --- /dev/null +++ b/app/src/main/res/values-uk/mime_types.xml @@ -0,0 +1,39 @@ + + + + + + Файл + Пакунок Android + %1$s архів + %1$s звук + %1$s календар + %1$s сертифікат + %1$s документ + Електронна візитка + Тека + %1$s документ + %1$s ел. книга + %1$s ел. лист + %1$s шрифт + %1$s файл + %1$s зображення + Документ PDF + %1$s презентація + %1$s таблиця + %1$s документ + Звичайний текстовий документ + %1$s відеозапис + Документ Word + Презентація PowerPoint + Таблиця Excel + Символьний пристрій + Блоковий пристрій + Канал + Посилання + Сокет + Посилання (зламане) + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 000000000..56d287284 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,677 @@ + + + + + + + Material Files + + Закрити + Копіювати + Вирізати + За замовченням + Видалити + Відхилити + Змінити + Порожньо + (Порожньо) + Помилка + Сховати + Встановити + Продовжити редагувати + Завантаження… + Може пізніше + Об\'єднати + Жодного + Відкрити панель навігації + Вставити + Оновити + Перезавантажити + Вилучити + Перейменувати + Замінити + Скинути + Повторити + Зберегти + Пошук + Вибрати + Вибрати все + Поширити + Показати + Пропустити + Зупинити + Невідомий + Вигляд + + + Для обробки цієї дії не знайдено програми + Відкрити налаштування + Скорочення створено + + + %1$,d байт + %1$,d байти + %1$,d байтів + %1$,d байтів + + + Застосунку потрібен дозвіл на керування файлами. Будь ласка, натисніть «Дозволити» у діалозі. + Програма потребує дозволу на доступ до файлів. Будь ласка, надайте доступ до «Cховища» в налаштуваннях системи. + Програмі потрібен дозвіл на керування файлами. Будь ласка, надайте дозвіл у майбутніх налаштуваннях системи. + + Фонові дії + Робити дії, коли програма у фоновому режимі + Операції з файлами + Відображати та контролювати файлові операції + FTP-сервер + Відображати та контролювати FTP сервер + + + Підготовка до стиснення %1$,d файлу (%2$s) + Підготовка до стиснення %1$,d файлів (%2$s) + Підготовка до стиснення %1$,d файлів (%2$s) + Підготовка до стиснення %1$,d файлів (%2$s) + + Стиснення «%1$s» у «%2$s» + + Стиснення %1$,d файлу в «%2$s» + Стиснення %1$,d файлів у «%2$s» + Стиснення %1$,d файлів у «%2$s» + Стиснення %1$,d файлів у «%2$s» + + + Підготовка до копіювання %1$,d файлу (%2$s) + Підготовка до копіювання %1$,d файлів (%2$s) + Підготовка до копіювання %1$,d файлів (%2$s) + Підготовка до копіювання %1$,d файлів (%2$s) + + Копіювання «%1$s» у «%2$s». + + Копіювання %1$,d файлу в «%2$s» + Копіювання %1$,d файлів у «%2$s» + Копіювання %1$,d файлів у «%2$s» + Копіювання %1$,d файлів у «%2$s» + + + Підготовка до видобування %1$,d файлу (%2$s) + Підготовка до видобування %1$,d файлів (%2$s) + Підготовка до видобування %1$,d файлів (%2$s) + Підготовка до видобування %1$,d файлів (%2$s) + + Видобування «%1$s» до «%2$s» + + Видобування %1$,d файлу до «%2$s» + Видобування %1$,d файлів «%2$s» + Видобування %1$,d файлів до «%2$s» + Видобування %1$,d файлів до «%2$s» + + + Підготовка до переміщення %1$,d файлу (%2$s) + Підготовка до переміщення %1$,d файлів (%2$s) + Підготовка до переміщення %1$,d файлів (%2$s) + Підготовка до переміщення %1$,d файлів (%2$s) + + Переміщення «%1$s» у «%2$s» + + Переміщення %1$,d файлів у «%2$s» + Переміщення %1$,d файлів у «%2$s» + Переміщення %1$,d файлів у «%2$s» + Переміщення %1$,d файлів у «%2$s» + + %1$s / %2$s + %1$,d / %2$,d + + Підготовка до видалення %1$,d файлу + Підготовка до видалення %1$,d файлів + Підготовка до видалення %1$,d файлів + Підготовка до видалення %1$,d файлів + + Видалення “%1$s” + + Видалення %1$,d файлу + Видалення %1$,d файлів + Видалення %1$,d файлів + Видалення %1$,d файлів + + + Підготовка до зміни власника для %1$,d файлу + Підготовка до зміни власника для %1$,d файлів + Підготовка до зміни власника для %1$,d файлів + Підготовка до зміни власника для %1$,d файлів + + Зміна власника на “%1$s” + + Зміна власника для %1$,d файлу + Зміна власника для %1$,d файлів + Зміна власника для %1$,d файлів + Зміна власника для %1$,d файлів + + + Підготовка до зміни групи для %1$,d файлу + Підготовка до зміни групи для %1$,d файлів + Підготовка до зміни групи для %1$,d файлів + Підготовка до зміни групи для %1$,d файлів + + Зміна групи на “%1$s” + + Зміна групи для %1$,d файлу + Зміна групи для %1$,d файлів + Зміна групи для %1$,d файлів + Зміна групи для %1$,d файлів + + + Підготовка до зміни режиму для %1$,d файлу + Підготовка до зміни режиму для %1$,d файлів + Підготовка до зміни режиму для %1$,d файлів + Підготовка до зміни режиму для %1$,d файлів + + Зміна режиму для “%1$s” + + Зміна режиму для %1$,d файлу + Зміна режиму для %1$,d файлів + Зміна режиму для %1$,d файлів + Зміна режиму для %1$,d файлів + + + Підготовка до зміни контексту SELinux %1$,d файлу + Підготовка до зміни контексту SELinux %1$,d файлів + Підготовка до зміни контексту SELinux %1$,d файлів + Підготовка до зміни контексту SELinux %1$,d файлів + + Зміна контексту SELinux для “%1$s” + + Зміна контексту SELinux для %1$,d файлу + Зміна контексту SELinux для %1$,d файлів + Зміна контексту SELinux для %1$,d файлів + Зміна контексту SELinux для %1$,d файлів + + + Підготовка до відновлення контексту SELinux для %1$,d файлу + Підготовка до відновлення контексту SELinux для %1$,d файлів + Підготовка до відновлення контексту SELinux для %1$,d файлів + Підготовка до відновлення контексту SELinux для %1$,d файлів + + Відновлення контексту SELinux для “%1$s” + + Відновлення контексту SELinux для %1$,d файлу + Відновлення контексту SELinux для %1$,d файлів + Відновлення контексту SELinux для %1$,d файлів + Відновлення контексту SELinux для %1$,d файлів + + %1$,d / %2$,d + Записується «%1$s» + Помилка під час створення + Сталася помилка під час створення «%1$s».\n%2$s + Неможливо копіювати теку в себе + Неможливо видобути теку в себе + Неможливо перемістити теку в себе + Цільова тека всередині вихідної теки. + Неможливо копіювати файл у себе + Неможливо видобути файл у себе + Неможливо перемістити файл у себе + Вихідний файл перезапишеться цільовим. + Замінити файл «%1$s»? + Файл з такою назвою вже існує у “%1$s”.\nЗамінивши його, ви перезапишете його вміст. + Оригінальний файл + Замінити на + Об\'єднати теку “%1$s”? + Під час об\'єднання запитуватиметься підтвердження перед копіюванням файлів, які конфліктують із тими, що вже є у теці. + Під час об\'єднання запитуватиметься підтвердження перед розпакуванням файлів, які конфліктують із тими, що вже є у теці. + Під час об\'єднання запитуватиметься підтвердження перед заміною файлів, які конфліктують із тими, що вже є у теці. + Оригінальна тека + Об\'єднати з + Виберіть нове ім\'я для цілі + Нова назва + Помилка під час стиснення «%1$s» + Сталася помилка під час стиснення файлу в «%1$s».\n%2$s + Помилка під час копіювання «%1$s» + Сталася помилка копіювання файлу в «%1$s».\n%2$s + Помилка під час видобування «%1$s» + Сталася помилка під час видобування файлу в «%1$s».\n%2$s + Помилка під час переміщення «%1$s» + Сталася помилка переміщення файлу в «%1$s».\n%2$s + Помилка під час видалення + Сталася помилка видалення «%1$s».\n%2$s + Помилка під час перейменування «%1$s» + Сталася помилка перейменування файлу на «%1$s».\n%2$s + Помилка під час зміни власника на «%1$s» + Сталася помилка зміни власника на «%1$s».\n%2$s + Помилка зміни групи на «%1$s» + Сталася помилка зміни групи на «%1$s».\n%2$s + Помилка зміни режиму на «%1$s» + Сталася помилка зміни режиму на «%1$s».\n%2$s + Помилка під час зміни контексту SELinux для “%1$s” + Сталася помилка під час зміни контексту SELinux на “%1$s”.\n%2$s + Помилка під час відновлення контексту SELinux + Сталася помилка під час відновлення контексту SELinux для “%1$s”.\n%2$s + Помилка під час запису + Сталася помилка під час запису «%1$s».\n%2$s + Сталася помилка під час отримання інформації про «%1$s». + Перемонтувати “%1$s” для читання/запису + “%1$s” перемонтовується для читання/запису… + “%1$s” перемонтовано для читання/запису + Застосувати цю дію для всіх файлів + + Цей файл є пакетом Android. Бажаєте встановити його чи переглянути його вміст? + Відкрити як… + Відкрити «%1$s» як + Текст + Зображення + Звук + Відео + Тека + Інше + “%1$s” готовий до встановлення + Торкніться, щоб встановити + “%1$s” готовий до відкриття + Торкніться, щоб відкрити + Назва файлу не може бути порожньою + Недійсне ім\'я файлу + Файл з такою назвою вже існує + Видалити «%1$s»? + Видалити теку «%1$s» і її вміст? + + Видалити %1$,d файл? + Видалити %1$,d файли? + Видалити %1$,d файлів? + Видалити %1$,d файлів? + + + Видалити %1$,d теку і її вміст? + Видалити %1$,d теки і їхній вміст? + Видалити %1$,d тек і їхній вміст? + Видалити %1$,d тек і їхній вміст? + + + Видалити %1$,d елемент? + Видалити %1$,d елементи? + Видалити %1$,d елементів? + Видалити %1$,d елементів? + + Створити архів + .zip + .tar.xz + .7z + Закладку додано + Новий файл + Нова тека + + Файли + + Вибрати файл + Вибрати файли + Вибрати файли + Вибрати файли + + + Вибарти теку + Вибрати теки + Вибрати теки + Вибрати теки + + + %1$,d тека + %1$,d теки + %1$,d тек + %1$,d тек + + + %1$,d файл + %1$,d файли + %1$,d файлів + %1$,d файлів + + ,\u0020 + Шлях не може бути порожнім + Недійсний шлях + Назва + Тип + Розмір + Востаннє змінено + Зростання + Теки спочатку + Нове вікно + Перейти вверх + Перейти в + Показати приховані файли + Копіювати шлях + Відкрити в терміналі + Додати закладку + Створити скорочення + Відкрити в новому вікні + Вибрати “%1$s” + Переміщення %1$,d + Копіювання %1$,d + Видобування %1$,d + Видобути тут + Нема файлів + Файл + Тека + + \u0020\u0020\u0020\u0020 + Відкрити через + Видобути + Стиснути + Властивості + + Властивості “%1$s” + Основні + Назва + Тип + %1$s (%2$s) + Посилається до %1$s(%2$s) + Ціль посилання + Розмір + %1$s (%2$s) + Вміст + + %1$,d предмет, з розміром %2$s + %1$,d предмети, всього %2$s + %1$,d предметів, всього %2$s + Предметів: %1$,d, всього %2$s + + Востаннє змінено + Батьківська тека + Файл архіву + Точка входу в архіві + Вільний простір + Дозволи + Власник + Група + %1$s (%2$d) + Змінити власника + Змінити групу + Введіть ім\'я або ID + Система + Режим + %1$s (%2$04o) + Змінити режим + Інше + + Читання + Запис + Виконання + + + Читання + Запис + Пошук + + Особливий + + Встановити UID + Встановити GID + Прапорець sticky bit + + Не додавати «Виконувати» для вкладених файлів + Контекст SELinux + Змінити контекст SELinux + Відновити + Застосувати до вкладених файлів + Зображення + Розміри + %1$d \u00d7 %2$d + Знято + Координати + %1$.3f, %2$.3f + Розташування + Висота + %1$,.3f м + Камера + %1$s %2$s + Апертура + Швидкість затвору + Фокусна відстань + %1$.2f мм + ISO + ISO %1$d + ПЗ + Опис + Автор + Авторські права + Аудіо + Заголовок + Виконавець + Альбом + Артист + Композитор + Диск + Доріжка + Рік + Жанр + Тривалість + Бітрейт + %1$d кб/с + Частота дискретизації + %1$d Гц + Відео + APK + Назва + Назва пакунка + Версія + %1$s (%2$d) + Найменша версія системи + Цільова версія системи + %1$s (%2$s, %3$d) + Дозволи + Дозволів не запитано + + Запитаний %1$,d дозвіл + Запитано %1$,d дозволи + Запитано %1$,d дозволів + %1$,d дозволів запитано + + Підписи + Немає дійсних підписів + Старі підписи + + Root + Сховище + Нема сховища + Додати сховище + Зовнішнє сховище + FTP-сервер + SFTP-сервер + SMB-сервер + Змінити сховище пристрою + Назва + Шлях + Додати зовнішнє сховище + Змінити зовнішнє сховище + Назва + URI + Шлях + Змінити FTP-сервер + Додати FTP-сервер + Назва хоста + Уведіть назву хоста + Недійсна назва хоста + Порт + Недійсний порт + Шлях + Може бути порожнім + Назва + Використовувати назву хоста + Протокол + Автентифікація + + Пароль + Анонім + + Імʼя користувача + Уведіть імʼя користувача + Пароль + Режим + + Активний + Пасивний + + Кодування + З\'єднатися та додати + Додати + Змінити SFTP-сервер + Додати SFTP-сервер + Назва хоста + Уведіть назву хоста + Недійсна назва хоста + Порт + Недійсний порт + Шлях + Може бути порожнім + Назва + Використовувати назву хоста + Автентифікація + + Пароль + Відкритий ключ + + Імʼя користувача + Уведіть імʼя користувача + Пароль + Закритий ключ + Відкрити файл + Уведіть закритий ключ + Недійсний закритий ключ + Пароль закритого ключа + Може бути порожнім + Недійсний пароль закритого ключа + З\'єднатися та додати + Додати + Пошук SMB-серверів… + Додати вручну + Змінити SMB-сервер + Додати SMB-сервер + Назва хоста + Уведіть назву хоста + Недійсна назва хоста + Порт + Недійсний порт + Шлях + Може бути порожнім + Назва + Використовувати назву хоста + Автентифікація + + Пароль + Гість + Анонім + + Імʼя користувача + Введіть ім\'я користувача + Пароль + Домен + З\'єднатися та додати + Додати + + %1$s вільно з %2$s + Додати сховище… + Нагадування + DCIM + Документи + Завантаження + Фільми + Музика + Сповіщення + Фотографії + Подкасти + Рінгтони + QQ + TIM + WeChat + Тека закладки + Назва + Шлях + Переглядач архівів + Переглядач зображень + %1$,d/%2$,d + Текстовий редактор + %1$s + *%1$s + Збережено + Ви впевнені, що хочете перезавантажити? Незбережені зміни в цьому документі буде втрачено. + Кодування + Ви впевнені, що бажаєте скасувати незбережені зміни в цьому документі? + + FTP-сервер + Стан + Запуск… + Працює + Зупинка… + Не запущено + URL + Копіювати URL + Копіювати пароль + Конфігурація + Анонімний вхід + Імʼя користувача + Пароль + Порт + Тека Root + Дозволити запис + Налаштування + Інтерфейс + Колір теми + Колір, який найчастіше з’являється у застосунку + Material Design 3 + Нічний режим + + Дотримуватись системи + Вимкнути + Увімкнути + На основі часу + На основі режиму економії батареї + + Чорний нічний режим + Анімація списку файлів + Відображати довгі назви файлів + + Еліпсувати початок + Еліпсувати середину + Еліпсувати кінець + \"Бігуча строка\" + + Поведінка + Типова тека + Стандартні теки + Нема ввімкнених стандартних тек + Знімки екрана + Теки закладок + Нема тек закладок + Режим доступу root + + У вас запущена %1$,d файлова операція, і зміна режиму root доступу зараз може призвести до неочікуваної помилки. Ви впевнені, що хочете змінити його зараз? + У вас запущено %1$,d файлові операції, і зміна режиму root доступу зараз може призвести до неочікуваної помилки. Ви впевнені, що хочете змінити його зараз? + У вас запущено %1$,d файлових операцій, і зміна режиму root доступу зараз може призвести до неочікуваної помилки. Ви впевнені, що хочете змінити його зараз? + У вас запущено %1$,d файлових операцій, і зміна режиму root доступу зараз може призвести до неочікуваної помилки. Ви впевнені, що хочете змінити його зараз? + + + Лише звичайний доступ + Автоматично + Лише доступ root + + Кодування назви файлу для архіву + Відкривати пакунок Android + + Встановити + Переглянути вміст + Запитати що робити + + Показувати мініатюри для PDF файлів + Може зробити застосунок нестабільним на старих версіях Android + Зчитувати віддалені файли для ескізів + + Про застосунок + Версія + Переглянути на GitHub + Ліцензії + Політика приватності + Автор + Hai Zhang + Стежити на GitHub + Стежити у Twitter + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 599c358f5..74b2c8c39 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -249,7 +249,6 @@ Thay đổi lần cuối Tăng dần Thư mục lên trước - Chỉ đối với thư mục này Cửa sổ mới Đi lên Đi đến @@ -259,7 +258,6 @@ Thêm đánh dấu Tạo lối tắt Mở trong cửa sổ mới - Đã chọn %1$,d Chọn “%1$s” Đang di chuyển %1$,d Đang sao chép %1$,d @@ -514,7 +512,6 @@ Đang dừng… Chưa khởi động URL - Không thể lấy địa chỉ IP nội bộ Sao chép URL Sao chép mật khẩu Thiết lập @@ -524,7 +521,6 @@ Cổng Thư mục gốc Cho phép ghi - Cài đặt Giao diện Màu chủ đề From 136efa242660edcb9dfa1ef6cdf3f51ca11b5016 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 27 Aug 2023 18:27:31 -0700 Subject: [PATCH 112/326] [Fix] Disable TV launcher intent due to Play policy. --- app/src/main/AndroidManifest.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 664e1b797..b811860b6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,7 +11,8 @@ - + + @@ -60,7 +61,8 @@ - + + From 7a80a3ac18db233c04bcc0de33317db83737e84b Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 27 Aug 2023 18:39:00 -0700 Subject: [PATCH 113/326] [Fix] Shorten changelog to make Google Play happy. --- fastlane/metadata/android/en-US/changelogs/32.txt | 6 +++--- fastlane/metadata/android/zh-CN/changelogs/32.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fastlane/metadata/android/en-US/changelogs/32.txt b/fastlane/metadata/android/en-US/changelogs/32.txt index 0f3a7a6a7..3b858a2d9 100644 --- a/fastlane/metadata/android/en-US/changelogs/32.txt +++ b/fastlane/metadata/android/en-US/changelogs/32.txt @@ -1,10 +1,10 @@ - Added grid view. - Added per-app language setting. -- Added shortcut to DocumentsUI for accessing Android/data and Android/obb. +- Added shortcut to access Android/data. - Added URL information to FTP server notification. - Added banner for Android TV. - Added Greek, Finnish, Lithuanian, Norwegian Bokmål and Ukrainian translation. - Fast scroll popup now shows text according to the current sort options. -- Video thumbnail is now taken from 1/3 of the video instead of the first frame. -- Material Design 2 theme will be removed in the upcoming version 1.7.0 to ease code maintenance. +- Video thumbnail is now taken from 1/3 of the video. +- Material Design 2 theme will be removed in the upcoming version 1.7.0. - Other bug fixes and improvements. diff --git a/fastlane/metadata/android/zh-CN/changelogs/32.txt b/fastlane/metadata/android/zh-CN/changelogs/32.txt index dcfbac644..3323fca76 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/32.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/32.txt @@ -1,10 +1,10 @@ - 添加了网格视图。 - 添加了分应用语言设置。 -- 添加了指向 DocumentsUI 的快捷方式以访问 Android/data 和 Android/obb。 +- 添加了访问 Android/data 的快捷方式。 - 向 FTP 服务器的通知添加了 URL 信息。 - 为 Android TV 添加了横幅。 - 添加了希腊语、芬兰语、立陶宛语、书面挪威语和乌克兰语翻译。 - 快速滚动的提示现在会根据排序选项来显示文字。 -- 视频缩略图现在会从视频的 1/3 处而非第一帧获取。 -- Material Design 2 主题将在未来的 1.7.0 版本中被移除以便于代码维护。 +- 视频缩略图现在会从视频的 1/3 处获取。 +- Material Design 2 主题将在未来的 1.7.0 版本中被移除。 - 其他错误修复和改进。 From 72f500c2833a51a7403a28f44d8dc80dc870ef33 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 27 Aug 2023 18:48:55 -0700 Subject: [PATCH 114/326] [Fix] Fix grammatical error in changelog. --- fastlane/metadata/android/en-US/changelogs/32.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/en-US/changelogs/32.txt b/fastlane/metadata/android/en-US/changelogs/32.txt index 3b858a2d9..f25fb2f19 100644 --- a/fastlane/metadata/android/en-US/changelogs/32.txt +++ b/fastlane/metadata/android/en-US/changelogs/32.txt @@ -3,7 +3,7 @@ - Added shortcut to access Android/data. - Added URL information to FTP server notification. - Added banner for Android TV. -- Added Greek, Finnish, Lithuanian, Norwegian Bokmål and Ukrainian translation. +- Added Greek, Finnish, Lithuanian, Norwegian Bokmål and Ukrainian translations. - Fast scroll popup now shows text according to the current sort options. - Video thumbnail is now taken from 1/3 of the video. - Material Design 2 theme will be removed in the upcoming version 1.7.0. From ec3710e18788ed283153baf982b9ea668bfe824f Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 28 Aug 2023 02:18:39 -0700 Subject: [PATCH 115/326] [Fix] Fix grid column count when persistent drawer is open. Fixes: #999 --- .../files/filelist/FileListFragment.kt | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index 25dce62f4..56396bc8a 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -285,11 +285,6 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. } } val viewLifecycleOwner = viewLifecycleOwner - if (binding.persistentDrawerLayout != null) { - Settings.FILE_LIST_PERSISTENT_DRAWER_OPEN.observe(viewLifecycleOwner) { - onPersistentDrawerOpenChanged(it) - } - } viewModel.currentPathLiveData.observe(viewLifecycleOwner) { onCurrentPathChanged(it) } viewModel.searchViewExpandedLiveData.observe(viewLifecycleOwner) { onSearchViewExpandedChanged(it) @@ -298,6 +293,14 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. binding.breadcrumbLayout.setData(it) } viewModel.viewTypeLiveData.observe(viewLifecycleOwner) { onViewTypeChanged(it) } + // Live data only calls observeForever() on its sources when it is active, so we have to + // make view type live data active first (so that it can load its initial value) before we + // register another observer that needs to get the view type. + if (binding.persistentDrawerLayout != null) { + Settings.FILE_LIST_PERSISTENT_DRAWER_OPEN.observe(viewLifecycleOwner) { + onPersistentDrawerOpenChanged(it) + } + } viewModel.sortOptionsLiveData.observe(viewLifecycleOwner) { onSortOptionsChanged(it) } viewModel.viewSortPathSpecificLiveData.observe(viewLifecycleOwner) { onViewSortPathSpecificChanged(it) @@ -513,6 +516,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. it.closeDrawer(GravityCompat.START) } } + updateSpanCount() } private fun onCurrentPathChanged(path: Path) { @@ -586,14 +590,27 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. } private fun onViewTypeChanged(viewType: FileViewType) { - layoutManager.spanCount = when (viewType) { - FileViewType.LIST -> 1 - FileViewType.GRID -> (resources.configuration.screenWidthDp / 180).coerceAtLeast(2) - } + updateSpanCount() adapter.viewType = viewType updateViewSortMenuItems() } + private fun updateSpanCount() { + layoutManager.spanCount = when (viewModel.viewType) { + FileViewType.LIST -> 1 + FileViewType.GRID -> { + var width = resources.configuration.screenWidthDp + val persistentDrawerLayout = binding.persistentDrawerLayout + if (persistentDrawerLayout != null && + persistentDrawerLayout.isDrawerOpen(GravityCompat.START)) { + // R.dimen.navigation_max_width + width -= 320 + } + (width / 180).coerceAtLeast(2) + } + } + } + private fun onSortOptionsChanged(sortOptions: FileSortOptions) { adapter.sortOptions = sortOptions updateViewSortMenuItems() From 8a5389f92732fc3f1dcbe38dc859470116021c42 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 29 Aug 2023 03:01:11 -0700 Subject: [PATCH 116/326] [Fix] Downgrade Commons Net to fix crash on lower APIs. Fixes: #1001 --- app/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 385f85f50..e3e870bd2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,7 +162,9 @@ dependencies { // com.google.guava:listenablefuture:1.0 pulled in by AndroidX Core implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' implementation 'com.takisoft.preferencex:preferencex:1.1.0' - implementation 'commons-net:commons-net:3.9.0' + // Commons Net 3.9.0 started using java.time.Duration in FTPClient. + //noinspection GradleDependency + implementation 'commons-net:commons-net:3.8.0' // LicensesDialog 2.2.0 pulls in androidx.webkit and uses setForceDark() instead of correctly // setting colors. //noinspection GradleDependency From 09138dee095edb7b5b90cfc67e4eb757d2aa8be4 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 29 Aug 2023 17:42:12 -0700 Subject: [PATCH 117/326] [Feature] Add SECURITY.md. Fixes: #1002 --- SECURITY.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..e7d7a2a53 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +Only the latest version of this app is currently being supported with security updates. + +## Reporting a Vulnerability + +Please email dreaming.in.code.zh@gmail.com to report a vulnerability. From 2354af0eb9887ea8d3de41f6e5806d4fa5c3ef3c Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 2 Sep 2023 16:34:02 -0700 Subject: [PATCH 118/326] [Feature] Always enable verbose logging in libsu. It's too hard to debug any libsu issue without this, and everyone is already so spammy in logs so adding a little bit more helpful information shouldn't be a problem. Bug: #1003 --- .../android/files/provider/root/LibSuFileServiceLauncher.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/root/LibSuFileServiceLauncher.kt b/app/src/main/java/me/zhanghai/android/files/provider/root/LibSuFileServiceLauncher.kt index afa7462b6..dd4e33e39 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/root/LibSuFileServiceLauncher.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/root/LibSuFileServiceLauncher.kt @@ -33,7 +33,7 @@ object LibSuFileServiceLauncher { private val lock = Any() init { - Shell.enableVerboseLogging = BuildConfig.DEBUG + Shell.enableVerboseLogging = true Shell.setDefaultBuilder( Shell.Builder.create() .setInitializers(LibSuShellInitializer::class.java) From dae309e0a4e68bfab22c348a30005ddea1f1dc39 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 2 Sep 2023 21:26:36 -0700 Subject: [PATCH 119/326] [Fix] Downgrade libsu to 5.1.0. To fix regression on OnePlus devices. Fixes: #1003 --- app/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e3e870bd2..36b040d93 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -103,7 +103,9 @@ repositories { dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' releaseImplementation 'com.github.mypplication:stetho-noop:1.1' - implementation 'com.github.topjohnwu.libsu:service:5.2.0' + // libsu 5.2.0 is buggy on OnePlus: https://github.com/topjohnwu/Magisk/issues/7214 + //noinspection GradleDependency + implementation 'com.github.topjohnwu.libsu:service:5.1.0' } dependencies { From e0ab378f50728d805b6687edac043a52a99d8dba Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 2 Sep 2023 23:25:21 -0700 Subject: [PATCH 120/326] [Feature] Import translations. --- app/src/main/res/values-uk/strings.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 56d287284..cc3338917 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -45,6 +45,7 @@ Показати Пропустити Зупинити + Системна за замовчуванням Невідомий Вигляд @@ -329,12 +330,16 @@ ,\u0020 Шлях не може бути порожнім Недійсний шлях + Вигляд та сортування + Список + Сітка Назва Тип Розмір Востаннє змінено Зростання Теки спочатку + Тільки для цієї теки Нове вікно Перейти вверх Перейти в @@ -344,6 +349,7 @@ Додати закладку Створити скорочення Відкрити в новому вікні + %1$,d Вибрати “%1$s” Переміщення %1$,d Копіювання %1$,d @@ -481,6 +487,10 @@ Змінити сховище пристрою Назва Шлях + Додати ярлик DocumentsUI + Редагувати ярлик DocumentsUI + Введіть URI + Невірний URI Додати зовнішнє сховище Змінити зовнішнє сховище Назва @@ -604,8 +614,13 @@ Зупинка… Не запущено URL + Локальна IP-адреса невідома Копіювати URL Копіювати пароль + Додати до швидких налаштувань + \"FTP сервер\" був доданий до швидких налаштувань + \"FTP сервер\" уже доданий до швидких налаштувань + Сталася помилка при додаванні \"FTP сервер\" до швидких налаштувань Конфігурація Анонімний вхід Імʼя користувача @@ -615,6 +630,7 @@ Дозволити запис Налаштування Інтерфейс + Мова Колір теми Колір, який найчастіше з’являється у застосунку Material Design 3 From 0bd9668ffbe6a7b2aac64bfe195aae3b1d69c5ba Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 2 Sep 2023 23:44:32 -0700 Subject: [PATCH 121/326] [Fix] Fix crash when URI is null in getDocumentManagerShortcutOrSetError(). --- .../storage/EditDocumentManagerShortcutDialogFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt index d6136b1a1..dfd81b6e6 100644 --- a/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/storage/EditDocumentManagerShortcutDialogFragment.kt @@ -78,8 +78,8 @@ class EditDocumentManagerShortcutDialogFragment : AppCompatDialogFragment() { errorEdit = binding.uriEdit } } - val uri = Uri.parse(uriText).asDocumentUriOrNull() - if (uri == null) { + val uri = uriText?.let { Uri.parse(it).asDocumentUriOrNull() } + if (uriText != null && uri == null) { binding.uriLayout.error = getString(R.string.storage_edit_document_manager_shortcut_uri_error_invalid) if (errorEdit == null) { From f029b21fbf99e4872eec72dd645befddd32990a1 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 3 Sep 2023 00:23:03 -0700 Subject: [PATCH 122/326] [Refactor] Fix calls to nullable toString(). --- .../fileproperties/permissions/SetPrincipalDialogFragment.kt | 2 +- .../zhanghai/android/files/provider/common/FileSystemCache.kt | 2 +- .../me/zhanghai/android/files/provider/content/ContentPath.kt | 2 +- .../me/zhanghai/android/files/viewer/text/TextEditorFragment.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetPrincipalDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetPrincipalDialogFragment.kt index 4d7c3be11..6a3746441 100644 --- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetPrincipalDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetPrincipalDialogFragment.kt @@ -56,7 +56,7 @@ abstract class SetPrincipalDialogFragment : AppCompatDialogFragment() { } binding = SetPrincipalDialogBinding.inflate(context.layoutInflater) - binding.filterEdit.doAfterTextChanged { viewModel.filter = it.toString() } + binding.filterEdit.doAfterTextChanged { viewModel.filter = it!!.toString() } binding.recyclerView.layoutManager = LinearLayoutManager(context) adapter = createAdapter(selectionLiveData) binding.recyclerView.adapter = adapter diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/FileSystemCache.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/FileSystemCache.kt index dd80007f9..eb84d8638 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/FileSystemCache.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/FileSystemCache.kt @@ -10,7 +10,7 @@ import java8.nio.file.FileSystemAlreadyExistsException import java8.nio.file.FileSystemNotFoundException import java.lang.ref.WeakReference -class FileSystemCache { +class FileSystemCache { private val fileSystems: MutableMap> = HashMap() private val lock = Any() diff --git a/app/src/main/java/me/zhanghai/android/files/provider/content/ContentPath.kt b/app/src/main/java/me/zhanghai/android/files/provider/content/ContentPath.kt index 5fd4f1cda..e1a621358 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/content/ContentPath.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/content/ContentPath.kt @@ -90,7 +90,7 @@ internal class ContentPath : ByteStringListPath { override fun normalize(): ContentPath = this - override fun toUri(): URI = URI.create(uri.toString()) + override fun toUri(): URI = URI.create(uri!!.toString()) override fun toAbsolutePath(): ContentPath { if (!isAbsolute) { diff --git a/app/src/main/java/me/zhanghai/android/files/viewer/text/TextEditorFragment.kt b/app/src/main/java/me/zhanghai/android/files/viewer/text/TextEditorFragment.kt index 1c5163dc2..1311feb24 100644 --- a/app/src/main/java/me/zhanghai/android/files/viewer/text/TextEditorFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/viewer/text/TextEditorFragment.kt @@ -142,7 +142,7 @@ class TextEditorFragment : Fragment(), ConfirmReloadDialogFragment.Listener, true } Menu.FIRST -> { - viewModel.encoding.value = Charset.forName(item.titleCondensed.toString()) + viewModel.encoding.value = Charset.forName(item.titleCondensed!!.toString()) true } else -> super.onOptionsItemSelected(item) From 612b1ef530609ab46218e3b08e43a560cc0b947a Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 3 Sep 2023 00:25:02 -0700 Subject: [PATCH 123/326] [Fix] Fix crash when copying/moving files from different providers. --- .../java/me/zhanghai/android/files/filelist/FileListFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index 56396bc8a..fa4b2be8c 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -1032,7 +1032,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. } private fun makePathListForJob(files: FileItemSet): List = - files.map { it.path }.sorted() + files.map { it.path }.sortedBy { it.toUri() } private fun onFileNameEllipsizeChanged(fileNameEllipsize: TextUtils.TruncateAt) { adapter.nameEllipsize = fileNameEllipsize From dfb365cbad1804cbf87cc399b5ac96e82b7bef4e Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 3 Sep 2023 00:47:09 -0700 Subject: [PATCH 124/326] [Feature] Bump version to 1.6.1. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 36b040d93..8691eb745 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,8 +35,8 @@ android { // Not supporting notification runtime permission yet. //noinspection OldTargetApi targetSdk 32 - versionCode 32 - versionName '1.6.0' + versionCode 33 + versionName '1.6.1' resValue 'string', 'app_version', versionName + ' (' + versionCode + ')' buildConfigField 'String', 'FILE_PROVIDIER_AUTHORITY', 'APPLICATION_ID + ".file_provider"' resValue 'string', 'app_provider_authority', applicationId + '.app_provider' From c70c01a3b6823bbb82bde85b789849dacf075dee Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 3 Sep 2023 00:47:22 -0700 Subject: [PATCH 125/326] [Feature] Add changelog for 1.6.1. --- fastlane/metadata/android/en-US/changelogs/33.txt | 4 ++++ fastlane/metadata/android/zh-CN/changelogs/33.txt | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/33.txt create mode 100644 fastlane/metadata/android/zh-CN/changelogs/33.txt diff --git a/fastlane/metadata/android/en-US/changelogs/33.txt b/fastlane/metadata/android/en-US/changelogs/33.txt new file mode 100644 index 000000000..05eb4ff28 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/33.txt @@ -0,0 +1,4 @@ +- Fixed FTP client not working before Android 8.0. +- Fixed root access not working on some OnePlus devices. +- Fixed number of grid view columns on landscape tablet. +- Other bug fixes and improvements. diff --git a/fastlane/metadata/android/zh-CN/changelogs/33.txt b/fastlane/metadata/android/zh-CN/changelogs/33.txt new file mode 100644 index 000000000..622af3334 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/33.txt @@ -0,0 +1,4 @@ +- 修复了 FTP 客户端在 Android 8.0 之前无法工作的问题。 +- 修复了 root 访问在部分一加设备上无法工作的问题。 +- 修复了在平板电脑的横屏模式下网格视图列数不正确的问题。 +- 其他错误修复和改进。 From 39f6312ca9f1d82f5cc0e46ef004a6495f45418a Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 3 Sep 2023 13:12:38 -0700 Subject: [PATCH 126/326] [Fix] Update xdg-shared-mime-info URL in README. --- README.md | 2 +- README_zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b41cfdda5..4438b4c1a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ An open source Material Design file manager, for Android 5.0+. [Get it on Coolapk](https://www.coolapk.com/apk/me.zhanghai.android.files) [Get the APK](https://github.com/zhanghai/MaterialFiles/releases/latest/download/app-release.apk) -[Help translation on Transifex](https://www.transifex.com/zhanghai/MaterialFiles/) ([Search Android & GNOME translations](https://translations.zhanghai.me/), [Search Microsoft translations](https://www.microsoft.com/en-us/language), [MIME type translations](https://github.com/freedesktop/xdg-shared-mime-info/tree/master/po)) +[Help translation on Transifex](https://www.transifex.com/zhanghai/MaterialFiles/) ([Search Android & GNOME translations](https://translations.zhanghai.me/), [Search Microsoft translations](https://www.microsoft.com/en-us/language), [MIME type translations](https://gitlab.freedesktop.org/xdg/shared-mime-info/-/tree/master/po)) ## Preview diff --git a/README_zh-CN.md b/README_zh-CN.md index ea86376c2..be446cf27 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -8,7 +8,7 @@ [在酷安上下载](https://www.coolapk.com/apk/me.zhanghai.android.files) [下载 APK](https://github.com/zhanghai/MaterialFiles/releases/latest/download/app-release.apk) -[在 Transifex 上帮助翻译](https://www.transifex.com/zhanghai/MaterialFiles/)([搜索 Android 和 GNOME 的翻译](https://translations.zhanghai.me/)、[搜索微软的翻译](https://www.microsoft.com/en-us/language)、[MIME 类型翻译](https://github.com/freedesktop/xdg-shared-mime-info/tree/master/po)) +[在 Transifex 上帮助翻译](https://www.transifex.com/zhanghai/MaterialFiles/)([搜索 Android 和 GNOME 的翻译](https://translations.zhanghai.me/)、[搜索微软的翻译](https://www.microsoft.com/en-us/language)、[MIME 类型翻译](https://gitlab.freedesktop.org/xdg/shared-mime-info/-/tree/master/po)) ## 预览 From 3fdbd6a908d80079ab06edbdb2c20da272089962 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 5 Sep 2023 02:20:08 -0700 Subject: [PATCH 127/326] [Feature] Update libsu to 5.2.1. See also https://github.com/topjohnwu/Magisk/issues/7214#issuecomment-1704732278 . --- app/build.gradle | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8691eb745..91bf9b40a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -103,9 +103,7 @@ repositories { dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' releaseImplementation 'com.github.mypplication:stetho-noop:1.1' - // libsu 5.2.0 is buggy on OnePlus: https://github.com/topjohnwu/Magisk/issues/7214 - //noinspection GradleDependency - implementation 'com.github.topjohnwu.libsu:service:5.1.0' + implementation 'com.github.topjohnwu.libsu:service:5.2.1' } dependencies { From 9f91ffb7c9b8617d6b4d4a906a152d9e31ba6cf4 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 8 Sep 2023 15:05:27 -0700 Subject: [PATCH 128/326] [Feature] Update dependencies. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 91bf9b40a..3920c268e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -118,11 +118,11 @@ dependencies { implementation 'androidx.activity:activity-ktx:1.7.2' // Appcompat 1.7.0-alpha01 is required for properly changing locale below API 24 (b/243119645). implementation 'androidx.appcompat:appcompat:1.7.0-alpha03' - implementation 'androidx.core:core-ktx:1.10.1' + implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.drawerlayout:drawerlayout:1.2.0' implementation 'androidx.exifinterface:exifinterface:1.3.6' implementation 'androidx.fragment:fragment-ktx:1.6.1' - def androidx_lifecycle_version = '2.6.1' + def androidx_lifecycle_version = '2.6.2' implementation "androidx.lifecycle:lifecycle-common-java8:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidx_lifecycle_version" implementation "androidx.lifecycle:lifecycle-process:$androidx_lifecycle_version" From 5b5838ba78c575290587ea60f5d7f7bd9470788c Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 8 Sep 2023 15:05:38 -0700 Subject: [PATCH 129/326] [Feature] Add getDimensionDp() and specify some missing return types. --- .../android/files/compat/TypedValueCompat.kt | 12 +++++ .../files/filelist/FileListFragment.kt | 9 ++-- .../android/files/util/ContextExtensions.kt | 43 +++++++++++++--- .../android/files/util/FragmentExtensions.kt | 49 ++++++++++++++----- .../files/util/TypedValueExtensions.kt | 27 ++++++++++ 5 files changed, 117 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/me/zhanghai/android/files/compat/TypedValueCompat.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/util/TypedValueExtensions.kt diff --git a/app/src/main/java/me/zhanghai/android/files/compat/TypedValueCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/TypedValueCompat.kt new file mode 100644 index 000000000..cd1d3dd81 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/compat/TypedValueCompat.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.compat + +import android.util.TypedValue +import androidx.core.util.TypedValueCompat + +val TypedValue.complexUnitCompat: Int + get() = TypedValueCompat.getUnitFromComplexDimension(data) diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index fa4b2be8c..e2b29468b 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -104,6 +104,7 @@ import me.zhanghai.android.files.util.createViewIntent import me.zhanghai.android.files.util.extraPath import me.zhanghai.android.files.util.extraPathList import me.zhanghai.android.files.util.fadeToVisibilityUnsafe +import me.zhanghai.android.files.util.getDimensionDp import me.zhanghai.android.files.util.getQuantityString import me.zhanghai.android.files.util.hasSw600Dp import me.zhanghai.android.files.util.isOrientationLandscape @@ -115,6 +116,7 @@ import me.zhanghai.android.files.util.valueCompat import me.zhanghai.android.files.util.viewModels import me.zhanghai.android.files.util.withChooser import me.zhanghai.android.files.viewer.image.ImageViewerActivity +import kotlin.math.roundToInt class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.Listener, OpenApkDialogFragment.Listener, ConfirmDeleteFilesDialogFragment.Listener, @@ -599,14 +601,13 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. layoutManager.spanCount = when (viewModel.viewType) { FileViewType.LIST -> 1 FileViewType.GRID -> { - var width = resources.configuration.screenWidthDp + var widthDp = resources.configuration.screenWidthDp val persistentDrawerLayout = binding.persistentDrawerLayout if (persistentDrawerLayout != null && persistentDrawerLayout.isDrawerOpen(GravityCompat.START)) { - // R.dimen.navigation_max_width - width -= 320 + widthDp -= getDimensionDp(R.dimen.navigation_max_width).roundToInt() } - (width / 180).coerceAtLeast(2) + (widthDp / 180).coerceAtLeast(2) } } } diff --git a/app/src/main/java/me/zhanghai/android/files/util/ContextExtensions.kt b/app/src/main/java/me/zhanghai/android/files/util/ContextExtensions.kt index adc40a718..13a1b8b0b 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/ContextExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/ContextExtensions.kt @@ -13,6 +13,7 @@ import android.content.ContextWrapper import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration +import android.content.res.Resources import android.graphics.drawable.Drawable import android.os.Bundle import android.os.Looper @@ -36,7 +37,9 @@ import androidx.annotation.PluralsRes import androidx.annotation.StyleRes import androidx.appcompat.view.ContextThemeWrapper import androidx.core.content.res.ResourcesCompat +import androidx.core.util.TypedValueCompat import me.zhanghai.android.files.R +import me.zhanghai.android.files.compat.complexUnitCompat import me.zhanghai.android.files.compat.getFloatCompat import me.zhanghai.android.files.compat.mainExecutorCompat import me.zhanghai.android.files.compat.obtainStyledAttributesCompat @@ -56,17 +59,38 @@ val Context.activity: Activity? fun Context.getAnimation(@AnimRes id: Int): Animation = AnimationUtils.loadAnimation(this, id) -fun Context.getBoolean(@BoolRes id: Int) = resources.getBoolean(id) +fun Context.getBoolean(@BoolRes id: Int): Boolean = resources.getBoolean(id) -fun Context.getDimension(@DimenRes id: Int) = resources.getDimension(id) +@Dimension +fun Context.getDimension(@DimenRes id: Int): Float = resources.getDimension(id) + +@Dimension(unit = Dimension.DP) +fun Context.getDimensionDp(@DimenRes id: Int): Float { + TypedValue::class.useTemp { value -> + resources.getValue(id, value, true) + if (value.type != TypedValue.TYPE_DIMENSION) { + throw Resources.NotFoundException( + "Resource ID #0x${Integer.toHexString(id)} type #0x${ + Integer.toHexString(value.type) + } is not valid" + ) + } + if (value.complexUnitCompat == TypedValue.COMPLEX_UNIT_DIP) { + return TypedValue.complexToFloat(value.data) + } + return dimensionToDp(TypedValue.complexToDimension(value.data, resources.displayMetrics)) + } +} -fun Context.getDimensionPixelOffset(@DimenRes id: Int) = resources.getDimensionPixelOffset(id) +@Dimension +fun Context.getDimensionPixelOffset(@DimenRes id: Int): Int = resources.getDimensionPixelOffset(id) -fun Context.getDimensionPixelSize(@DimenRes id: Int) = resources.getDimensionPixelSize(id) +@Dimension +fun Context.getDimensionPixelSize(@DimenRes id: Int): Int = resources.getDimensionPixelSize(id) -fun Context.getFloat(@DimenRes id: Int) = resources.getFloatCompat(id) +fun Context.getFloat(@DimenRes id: Int): Float = resources.getFloatCompat(id) -fun Context.getInteger(@IntegerRes id: Int) = resources.getInteger(id) +fun Context.getInteger(@IntegerRes id: Int): Int = resources.getInteger(id) fun Context.getInterpolator(@InterpolatorRes id: Int): Interpolator = AnimationUtils.loadInterpolator(this, id) @@ -162,6 +186,13 @@ fun Context.dpToDimensionPixelSize(@Dimension(unit = Dimension.DP) dp: Float): I fun Context.dpToDimensionPixelSize(@Dimension(unit = Dimension.DP) dp: Int) = dpToDimensionPixelSize(dp.toFloat()) +@Dimension(unit = Dimension.DP) +fun Context.dimensionToDp(@Dimension dimension: Float): Float = + TypedValueCompat.pxToDp(dimension, resources.displayMetrics) + +@Dimension(unit = Dimension.DP) +fun Context.dimensionToDp(@Dimension dimension: Int): Float = dimensionToDp(dimension.toFloat()) + fun Context.hasSwDp(@Dimension(unit = Dimension.DP) dp: Int): Boolean = resources.configuration.smallestScreenWidthDp >= dp diff --git a/app/src/main/java/me/zhanghai/android/files/util/FragmentExtensions.kt b/app/src/main/java/me/zhanghai/android/files/util/FragmentExtensions.kt index 0396eec72..869c39180 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/FragmentExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/FragmentExtensions.kt @@ -7,7 +7,11 @@ package me.zhanghai.android.files.util import android.content.ActivityNotFoundException import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable import android.os.Bundle +import android.view.animation.Animation +import android.view.animation.Interpolator import android.widget.Toast import androidx.annotation.AnimRes import androidx.annotation.AnyRes @@ -29,34 +33,45 @@ import me.zhanghai.android.files.compat.getColorCompat import me.zhanghai.android.files.compat.getColorStateListCompat import me.zhanghai.android.files.compat.getDrawableCompat -fun Fragment.checkSelfPermission(permission: String) = +fun Fragment.checkSelfPermission(permission: String): Int = requireContext().checkSelfPermissionCompat(permission) -fun Fragment.finish() = requireActivity().finish() +fun Fragment.finish() { + requireActivity().finish() +} -fun Fragment.getAnimation(@AnimRes id: Int) = requireContext().getAnimation(id) +fun Fragment.getAnimation(@AnimRes id: Int): Animation = requireContext().getAnimation(id) -fun Fragment.getBoolean(@BoolRes id: Int) = requireContext().getBoolean(id) +fun Fragment.getBoolean(@BoolRes id: Int): Boolean = requireContext().getBoolean(id) @ColorInt -fun Fragment.getColor(@ColorRes id: Int) = requireContext().getColorCompat(id) +fun Fragment.getColor(@ColorRes id: Int): Int = requireContext().getColorCompat(id) + +fun Fragment.getColorStateList(@ColorRes id: Int): ColorStateList = + requireContext().getColorStateListCompat(id) -fun Fragment.getColorStateList(@ColorRes id: Int) = requireContext().getColorStateListCompat(id) +@Dimension +fun Fragment.getDimension(@DimenRes id: Int): Float = requireContext().getDimension(id) -fun Fragment.getDimension(@DimenRes id: Int) = requireContext().getDimension(id) +@Dimension(unit = Dimension.DP) +fun Fragment.getDimensionDp(@DimenRes id: Int): Float = requireContext().getDimensionDp(id) -fun Fragment.getDimensionPixelOffset(@DimenRes id: Int) = +@Dimension +fun Fragment.getDimensionPixelOffset(@DimenRes id: Int): Int = requireContext().getDimensionPixelOffset(id) -fun Fragment.getDimensionPixelSize(@DimenRes id: Int) = requireContext().getDimensionPixelSize(id) +@Dimension +fun Fragment.getDimensionPixelSize(@DimenRes id: Int): Int = + requireContext().getDimensionPixelSize(id) -fun Fragment.getDrawable(@DrawableRes id: Int) = requireContext().getDrawableCompat(id) +fun Fragment.getDrawable(@DrawableRes id: Int): Drawable = requireContext().getDrawableCompat(id) -fun Fragment.getFloat(@DimenRes id: Int) = requireContext().getFloat(id) +fun Fragment.getFloat(@DimenRes id: Int): Float = requireContext().getFloat(id) -fun Fragment.getInteger(@IntegerRes id: Int) = requireContext().getInteger(id) +fun Fragment.getInteger(@IntegerRes id: Int): Int = requireContext().getInteger(id) -fun Fragment.getInterpolator(@InterpolatorRes id: Int) = requireContext().getInterpolator(id) +fun Fragment.getInterpolator(@InterpolatorRes id: Int): Interpolator = + requireContext().getInterpolator(id) fun Fragment.getQuantityString(@PluralsRes id: Int, quantity: Int): String = requireContext().getQuantityString(id, quantity) @@ -123,6 +138,14 @@ fun Fragment.dpToDimensionPixelSize(@Dimension(unit = Dimension.DP) dp: Float) = fun Fragment.dpToDimensionPixelSize(@Dimension(unit = Dimension.DP) dp: Int) = requireContext().dpToDimensionPixelSize(dp) +@Dimension(unit = Dimension.DP) +fun Fragment.dimensionToDp(@Dimension dimension: Float): Float = + requireContext().dimensionToDp(dimension) + +@Dimension(unit = Dimension.DP) +fun Fragment.dimensionToDp(@Dimension dimension: Int): Float = + requireContext().dimensionToDp(dimension) + fun Fragment.setResult(resultCode: Int, resultData: Intent? = null) = requireActivity().setResult(resultCode, resultData) diff --git a/app/src/main/java/me/zhanghai/android/files/util/TypedValueExtensions.kt b/app/src/main/java/me/zhanghai/android/files/util/TypedValueExtensions.kt new file mode 100644 index 000000000..e274d0e4e --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/util/TypedValueExtensions.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.util + +import android.util.TypedValue +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KClass + +inline fun KClass.useTemp(block: (TypedValue) -> T): T { + val temp = TypedValue::class.obtainTemp() + return try { + block(temp) + } finally { + temp.releaseTemp() + } +} + +private val tempTypedValue = AtomicReference(TypedValue()) + +fun KClass.obtainTemp(): TypedValue = tempTypedValue.getAndSet(null) ?: TypedValue() + +fun TypedValue.releaseTemp() { + tempTypedValue.compareAndSet(null, this) +} From 308fed9d0da02bce6a387a59b80e096d458240ab Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 9 Sep 2023 02:05:01 -0700 Subject: [PATCH 130/326] [Refactor] Reorder build file entries. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 3920c268e..a891a829a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,9 +26,9 @@ apply plugin: 'com.google.firebase.crashlytics' android { namespace 'me.zhanghai.android.files' + buildToolsVersion = '34.0.0' compileSdk 34 ndkVersion '25.2.9519653' - buildToolsVersion = '34.0.0' defaultConfig { applicationId 'me.zhanghai.android.files' minSdk 21 From 72c2a4951128e52538a88f86633fe7a9698fada7 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 9 Sep 2023 02:08:20 -0700 Subject: [PATCH 131/326] [Feature] Update Firebase. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a891a829a..339466e99 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,7 +17,7 @@ buildscript { } dependencies { classpath 'com.google.gms:google-services:4.3.15' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.8' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' } } apply plugin: 'com.google.gms.google-services' @@ -213,7 +213,7 @@ dependencies { //#ifdef NONFREE implementation 'com.github.junrar:junrar:7.5.4' - implementation platform('com.google.firebase:firebase-bom:32.2.2') + implementation platform('com.google.firebase:firebase-bom:32.2.3') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-crashlytics-ndk' //#endif From 8c878fd4f5d6e1239f2073c9eedc6d06fbf76f20 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 9 Sep 2023 18:54:55 -0700 Subject: [PATCH 132/326] [Fix] Fix wrong exception being thrown. --- .../android/files/provider/archive/LocalArchiveFileStore.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileStore.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileStore.kt index 714c1a139..85ee12fa2 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileStore.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileStore.kt @@ -11,7 +11,6 @@ import me.zhanghai.android.files.file.MimeType import me.zhanghai.android.files.file.guessFromPath import me.zhanghai.android.files.provider.common.PosixFileStore import me.zhanghai.android.files.provider.common.size -import org.tukaani.xz.UnsupportedOptionsException import java.io.IOException internal class LocalArchiveFileStore(private val archiveFile: Path) : PosixFileStore() { @@ -25,7 +24,7 @@ internal class LocalArchiveFileStore(private val archiveFile: Path) : PosixFileS @Throws(IOException::class) override fun setReadOnly(readOnly: Boolean) { - throw UnsupportedOptionsException() + throw UnsupportedOperationException() } @Throws(IOException::class) From 7b76eb702934948864de1cea4a06de8cfec20cbd Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 10 Sep 2023 00:14:29 -0700 Subject: [PATCH 133/326] [Feature] Update MIME types. --- .../java/me/zhanghai/android/files/compat/MimeTypeMapCompat.kt | 2 ++ mime/MimeTypeMapCompat.kt | 2 ++ mime/android.extensions | 2 ++ mime/generate-extensions.sh | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/zhanghai/android/files/compat/MimeTypeMapCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/MimeTypeMapCompat.kt index 1702eb78e..226d9947a 100644 --- a/app/src/main/java/me/zhanghai/android/files/compat/MimeTypeMapCompat.kt +++ b/app/src/main/java/me/zhanghai/android/files/compat/MimeTypeMapCompat.kt @@ -46,6 +46,7 @@ private val extensionToMimeTypeMap = mapOf( "atomsrv" to "application/atomserv+xml", "au" to "audio/basic", "avi" to "video/avi", + "avif" to "image/avif", "awb" to "audio/amr-wb", "axa" to "audio/annodex", "axv" to "video/annodex", @@ -119,6 +120,7 @@ private val extensionToMimeTypeMap = mapOf( "ddeb" to "application/vnd.debian.binary-package", "deb" to "application/x-debian-package", "deploy" to "application/octet-stream", + "der" to "application/x-x509-ca-cert", "dfxp" to "application/ttml+xml", "dif" to "video/dv", "diff" to "text/plain", diff --git a/mime/MimeTypeMapCompat.kt b/mime/MimeTypeMapCompat.kt index 120fce3ad..b5209b3a3 100644 --- a/mime/MimeTypeMapCompat.kt +++ b/mime/MimeTypeMapCompat.kt @@ -36,6 +36,7 @@ private val extensionToMimeTypeMap = mapOf( "atomsrv" to "application/atomserv+xml", "au" to "audio/basic", "avi" to "video/avi", + "avif" to "image/avif", "awb" to "audio/amr-wb", "axa" to "audio/annodex", "axv" to "video/annodex", @@ -109,6 +110,7 @@ private val extensionToMimeTypeMap = mapOf( "ddeb" to "application/vnd.debian.binary-package", "deb" to "application/x-debian-package", "deploy" to "application/octet-stream", + "der" to "application/x-x509-ca-cert", "dfxp" to "application/ttml+xml", "dif" to "video/dv", "diff" to "text/plain", diff --git a/mime/android.extensions b/mime/android.extensions index f9dcca277..a9a9b5761 100644 --- a/mime/android.extensions +++ b/mime/android.extensions @@ -35,6 +35,7 @@ atomcat application/atomcat+xml atomsrv application/atomserv+xml au audio/basic avi video/avi +avif image/avif awb audio/amr-wb axa audio/annodex axv video/annodex @@ -108,6 +109,7 @@ dcr application/x-director ddeb application/vnd.debian.binary-package deb application/x-debian-package deploy application/octet-stream +der application/x-x509-ca-cert dfxp application/ttml+xml dif video/dv diff text/plain diff --git a/mime/generate-extensions.sh b/mime/generate-extensions.sh index f86edc480..21b4e77e1 100755 --- a/mime/generate-extensions.sh +++ b/mime/generate-extensions.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -BRANCH=master +BRANCH=main debian_mime_types=$(mktemp) android_mime_types=$(mktemp) From f80790dfdb7279134fd517be77fd40dced8e4539 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 9 Sep 2023 08:33:15 -0700 Subject: [PATCH 134/326] [Feature] Use libarchive. Fixes: #735 --- app/build.gradle | 12 +- .../files/file/MimeTypeTypeExtensions.kt | 16 +- .../android/files/filejob/FileJobService.kt | 7 +- .../android/files/filejob/FileJobs.kt | 9 +- .../filelist/CreateArchiveDialogFragment.kt | 26 +- .../files/filelist/FileListFragment.kt | 7 +- .../android/files/nonfree/RarArchiveEntry.kt | 33 -- .../files/nonfree/RarChannelVolumeManager.kt | 107 ----- .../zhanghai/android/files/nonfree/RarFile.kt | 108 ----- .../provider/archive/ArchiveFileAttributes.kt | 93 +--- .../provider/archive/ArchiveFileSystem.kt | 4 +- .../archive/LocalArchiveFileSystem.kt | 8 +- .../archive/archiver/ArchiveException.kt | 23 - .../archive/archiver/ArchiveReader.kt | 432 ++++-------------- .../archive/archiver/ArchiveWriter.kt | 228 ++------- .../provider/archive/archiver/ReadArchive.kt | 249 ++++++++++ .../provider/archive/archiver/WriteArchive.kt | 147 ++++++ .../archive/archiver/ZipFileCompat.kt | 100 ---- .../files/provider/common/PosixFileMode.kt | 19 +- .../files/provider/common/PosixFileType.kt | 65 +-- app/src/main/res/raw/licenses.xml | 49 +- 21 files changed, 637 insertions(+), 1105 deletions(-) delete mode 100644 app/src/main/java/me/zhanghai/android/files/nonfree/RarArchiveEntry.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/nonfree/RarChannelVolumeManager.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/nonfree/RarFile.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveException.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt delete mode 100644 app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ZipFileCompat.kt diff --git a/app/build.gradle b/app/build.gradle index 339466e99..83cdd996a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,18 +187,11 @@ dependencies { implementation 'me.zhanghai.android.appiconloader:appiconloader:1.5.0' implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.foregroundcompat:library:1.0.2' + implementation 'me.zhanghai.android.libarchive:library:1.0.0' implementation 'me.zhanghai.android.libselinux:library:2.1.0' implementation 'me.zhanghai.android.retrofile:library:1.1.1' implementation 'me.zhanghai.android.systemuihelper:library:1.0.0' implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.4' - // Commons Compress 1.21 requires Android 8.0. - // See also https://issues.apache.org/jira/browse/COMPRESS-611 - // Run `git revert fb1628e` if we ever created a fork that supports Android 5.0, or we raised - // minimum SDK version to Android 8.0. - //noinspection GradleDependency - implementation 'org.apache.commons:commons-compress:1.20' - // Optional dependency of Commons Compress for 7Z LZMA. - implementation 'org.tukaani:xz:1.9' implementation 'org.apache.ftpserver:ftpserver-core:1.2.0' // This is a dependency of org.apache.ftpserver:ftpserver-core but org.apache.mina:mina-core // 2.1.3+ became incompatible before API 24 due to dependency on StandardSocketOptions @@ -208,11 +201,10 @@ dependencies { strictly '2.1.3' } } - // Also a dependency of jCIFS-NG and Junrar. + // Also a dependency of jCIFS-NG. implementation 'org.slf4j:slf4j-android:1.7.36' //#ifdef NONFREE - implementation 'com.github.junrar:junrar:7.5.4' implementation platform('com.google.firebase:firebase-bom:32.2.3') implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-crashlytics-ndk' diff --git a/app/src/main/java/me/zhanghai/android/files/file/MimeTypeTypeExtensions.kt b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeTypeExtensions.kt index 1a89c462b..184a8b866 100644 --- a/app/src/main/java/me/zhanghai/android/files/file/MimeTypeTypeExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeTypeExtensions.kt @@ -5,8 +5,6 @@ package me.zhanghai.android.files.file -import android.os.Build - val MimeType.isApk: Boolean get() = this == MimeType.APK @@ -20,24 +18,24 @@ private val supportedArchiveMimeTypes = mutableListOf( "application/zip", "application/vnd.android.package-archive", "application/vnd.debian.binary-package", + "application/x-7z-compressed", "application/x-bzip2", + "application/x-cab", "application/x-compress", "application/x-cpio", "application/x-deb", "application/x-debian-package", "application/x-gtar", "application/x-gtar-compressed", + "application/x-iso9660-image", "application/x-java-archive", + "application/x-lha", "application/x-lzma", + "application/x-redhat-package-manager", "application/x-tar", + "application/x-ustar", "application/x-xz" -) - .apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - this += "application/x-7z-compressed" - } - } - .map { it.asMimeType() }.toSet() +).map { it.asMimeType() }.toSet() val MimeType.isImage: Boolean get() = icon == MimeTypeIcon.IMAGE diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt index c5b7b2e7e..ef34013e1 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt @@ -98,11 +98,12 @@ class FileJobService : Service() { fun archive( sources: List, archiveFile: Path, - archiveType: String, - compressorType: String?, + format: Int, + filter: Int, + password: String?, context: Context ) { - startJob(ArchiveFileJob(sources, archiveFile, archiveType, compressorType), context) + startJob(ArchiveFileJob(sources, archiveFile, format, filter, password), context) } fun copy(sources: List, targetDirectory: Path, context: Context) { diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt index 72435bacb..7fb6e2344 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt @@ -582,8 +582,9 @@ private class ActionAllInfo( class ArchiveFileJob( private val sources: List, private val archiveFile: Path, - private val archiveType: String, - private val compressorType: String? + private val format: Int, + private val filter: Int, + private val password: String? ) : FileJob() { @Throws(IOException::class) override fun run() { @@ -594,16 +595,16 @@ class ArchiveFileJob( var successful = false try { channel.use { - ArchiveWriter(archiveType, compressorType, channel).use { writer -> + ArchiveWriter(channel, format, filter, password).use { writer -> val transferInfo = TransferInfo(scanInfo, archiveFile) for (source in sources) { val target = getTargetFileName(source) archiveRecursively(source, writer, target, transferInfo) throwIfInterrupted() } - successful = true } } + successful = true } finally { if (!successful) { try { diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt index a3a0f6224..15263dc32 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt @@ -23,8 +23,7 @@ import me.zhanghai.android.files.util.args import me.zhanghai.android.files.util.putArgs import me.zhanghai.android.files.util.setTextWithSelection import me.zhanghai.android.files.util.show -import org.apache.commons.compress.archivers.ArchiveStreamFactory -import org.apache.commons.compress.compressors.CompressorStreamFactory +import me.zhanghai.android.libarchive.Archive class CreateArchiveDialogFragment : FileNameDialogFragment() { private val args by args() @@ -72,24 +71,13 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() { } override fun onOk(name: String) { - val archiveType: String - val compressorType: String? - when (val typeId = binding.typeGroup.checkedRadioButtonId) { - R.id.zipRadio -> { - archiveType = ArchiveStreamFactory.ZIP - compressorType = null - } - R.id.tarXzRadio -> { - archiveType = ArchiveStreamFactory.TAR - compressorType = CompressorStreamFactory.XZ - } - R.id.sevenZRadio -> { - archiveType = ArchiveStreamFactory.SEVEN_Z - compressorType = null - } + val (format, filter) = when (val typeId = binding.typeGroup.checkedRadioButtonId) { + R.id.zipRadio -> Archive.FORMAT_ZIP to Archive.FILTER_NONE + R.id.tarXzRadio -> Archive.FORMAT_TAR to Archive.FILTER_XZ + R.id.sevenZRadio -> Archive.FORMAT_7ZIP to Archive.FILTER_NONE else -> throw AssertionError(typeId) } - listener.archive(args.files, name, archiveType, compressorType) + listener.archive(args.files, name, format, filter, null) } companion object { @@ -120,6 +108,6 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() { } interface Listener : FileNameDialogFragment.Listener { - fun archive(files: FileItemSet, name: String, archiveType: String, compressorType: String?) + fun archive(files: FileItemSet, name: String, format: Int, filter: Int, password: String?) } } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index e2b29468b..2443724e4 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -911,12 +911,13 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. override fun archive( files: FileItemSet, name: String, - archiveType: String, - compressorType: String? + format: Int, + filter: Int, + password: String? ) { val archiveFile = viewModel.currentPath.resolve(name) FileJobService.archive( - makePathListForJob(files), archiveFile, archiveType, compressorType, requireContext() + makePathListForJob(files), archiveFile, format, filter, password, requireContext() ) viewModel.selectFiles(files, false) } diff --git a/app/src/main/java/me/zhanghai/android/files/nonfree/RarArchiveEntry.kt b/app/src/main/java/me/zhanghai/android/files/nonfree/RarArchiveEntry.kt deleted file mode 100644 index c2ffd4fa2..000000000 --- a/app/src/main/java/me/zhanghai/android/files/nonfree/RarArchiveEntry.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2019 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.nonfree - -import com.github.junrar.rarfile.FileHeader -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipEncoding -import java.util.Date - -class RarArchiveEntry(val header: FileHeader, zipEncoding: ZipEncoding) : ArchiveEntry { - private val name: String - - init { - @Suppress("DEPRECATION") - var name = header.fileNameW - if (name.isNullOrEmpty()) { - name = zipEncoding.decode(header.fileNameByteArray) - } - name = name.replace('\\', '/') - this.name = name - } - - override fun getName(): String = name - - override fun getSize(): Long = header.fullUnpackSize - - override fun isDirectory(): Boolean = header.isDirectory - - override fun getLastModifiedDate(): Date = header.mTime -} diff --git a/app/src/main/java/me/zhanghai/android/files/nonfree/RarChannelVolumeManager.kt b/app/src/main/java/me/zhanghai/android/files/nonfree/RarChannelVolumeManager.kt deleted file mode 100644 index e8596371f..000000000 --- a/app/src/main/java/me/zhanghai/android/files/nonfree/RarChannelVolumeManager.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2022 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.nonfree - -import com.github.junrar.Archive -import com.github.junrar.io.SeekableReadOnlyByteChannel -import com.github.junrar.volume.Volume -import com.github.junrar.volume.VolumeManager -import java8.nio.channels.SeekableByteChannel -import me.zhanghai.android.files.compat.withInitial -import java.io.EOFException -import java.io.IOException -import java.nio.ByteBuffer - -internal class RarChannelVolumeManager( - private val channel: SeekableByteChannel -) : VolumeManager { - override fun nextVolume(archive: Archive, lastVolume: Volume?): Volume? = - if (lastVolume == null) RarChannelVolume(archive, channel) else null -} - -private class RarChannelVolume( - private val archive: Archive, - private val channel: SeekableByteChannel -) : Volume { - private val delegateChannel = DelegateSeekableReadOnlyByteChannel(channel) - - @Throws(IOException::class) - override fun getChannel(): SeekableReadOnlyByteChannel = delegateChannel - - override fun getLength(): Long = - try { - channel.size() - } catch (e: IOException) { - e.printStackTrace() - 0 - } - - override fun getArchive(): Archive = archive -} - -private class DelegateSeekableReadOnlyByteChannel( - private val channel: SeekableByteChannel -) : SeekableReadOnlyByteChannel { - private val SINGLE_BYTE_BUFFER = ThreadLocal::class.withInitial { ByteBuffer.allocate(1) } - - @Throws(IOException::class) - override fun getPosition(): Long = channel.position() - - @Throws(IOException::class) - override fun setPosition(pos: Long) { - channel.position(pos) - } - - @Throws(IOException::class) - override fun read(): Int { - val buffer = SINGLE_BYTE_BUFFER.get()!! - buffer.clear() - while (true) { - when (channel.read(buffer)) { - -1 -> return -1 - 0 -> continue - else -> return buffer[0].toInt() and 0xFF - } - } - } - - @Throws(IOException::class) - override fun read(buffer: ByteArray, off: Int, count: Int): Int { - if (buffer.isEmpty()) { - return 0 - } - val byteBuffer = ByteBuffer.wrap(buffer, off, count) - while (true) { - val bytesRead = channel.read(byteBuffer) - if (bytesRead == 0) { - continue - } - return bytesRead - } - } - - @Throws(IOException::class) - override fun readFully(buffer: ByteArray, count: Int): Int { - require(count <= buffer.size) { - "count > buffer.size: count = $count, buffer.size = ${buffer.size}" - } - if (count == 0) { - return 0 - } - val byteBuffer = ByteBuffer.wrap(buffer, 0, count) - while (byteBuffer.hasRemaining()) { - if (channel.read(byteBuffer) == -1) { - throw EOFException() - } - } - return count - } - - @Throws(IOException::class) - override fun close() { - channel.close() - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/nonfree/RarFile.kt b/app/src/main/java/me/zhanghai/android/files/nonfree/RarFile.kt deleted file mode 100644 index d278c0b16..000000000 --- a/app/src/main/java/me/zhanghai/android/files/nonfree/RarFile.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2019 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.nonfree - -import com.github.junrar.Archive -import com.github.junrar.exception.RarException -import java8.nio.channels.SeekableByteChannel -import java8.nio.file.Path -import me.zhanghai.android.files.provider.archive.archiver.ArchiveException -import me.zhanghai.android.files.provider.common.newByteChannel -import org.apache.commons.compress.archivers.zip.ZipEncodingHelper -import org.apache.commons.compress.utils.IOUtils -import java.io.Closeable -import java.io.IOException -import java.io.InputStream -import java.io.PipedInputStream -import java.io.PipedOutputStream -import kotlin.math.max - -class RarFile(channel: SeekableByteChannel, encoding: String?) : Closeable { - private var archive = - try { - Archive(RarChannelVolumeManager(channel), null, null) - } catch (e: RarException) { - throw ArchiveException(e) - } - - private val zipEncoding = ZipEncodingHelper.getZipEncoding(encoding) - - @get:Throws(IOException::class) - val nextEntry: RarArchiveEntry? - get() = archive.nextFileHeader()?.let { RarArchiveEntry(it, zipEncoding) } - - @get:Throws(IOException::class) - val entries: Iterable - get() { - val entries = mutableListOf() - for (header in archive.fileHeaders) { - entries += RarArchiveEntry(header, zipEncoding) - } - return entries - } - - @Throws(IOException::class) - fun getInputStream(entry: RarArchiveEntry): InputStream { - val inputStream = PipedInputStream() - val outputStream = PipedOutputStream(inputStream) - Thread { - try { - outputStream.use { archive.extractFile(entry.header, it) } - } catch (e: IOException) { - e.printStackTrace() - } catch (e: RarException) { - e.printStackTrace() - } - }.start() - return inputStream - } - - @Throws(IOException::class) - override fun close() { - archive.close() - } - - companion object { - const val RAR = "rar" - - private val SIGNATURE_OLD = byteArrayOf(0x52, 0x45, 0x7e, 0x5e) - private val SIGNATURE_V4 = byteArrayOf(0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00) - private val SIGNATURE_V5 = byteArrayOf(0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01) - - @Throws(IOException::class) - fun detect(inputStream: InputStream): String? { - require(inputStream.markSupported()) { "InputStream.markSupported() returned false" } - val signature = ByteArray(max(SIGNATURE_OLD.size, SIGNATURE_V4.size)) - inputStream.mark(signature.size) - val signatureLength = try { - IOUtils.readFully(inputStream, signature) - } finally { - inputStream.reset() - } - return if (matches(signature, signatureLength)) RAR else null - } - - private fun matches(signature: ByteArray, length: Int): Boolean = - matches(signature, length, SIGNATURE_OLD) - || matches(signature, length, SIGNATURE_V4) - || matches(signature, length, SIGNATURE_V5) - - private fun matches(actual: ByteArray, actualLength: Int, expected: ByteArray): Boolean { - if (actualLength < expected.size) { - return false - } - for (index in expected.indices) { - if (actual[index] != expected[index]) { - return false - } - } - return true - } - - fun create(file: Path, encoding: String?): RarFile = - RarFile(file.newByteChannel(), encoding) - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt index b462a8b62..dfb161433 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt @@ -10,22 +10,14 @@ import java8.nio.file.Path import java8.nio.file.attribute.FileTime import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith +import me.zhanghai.android.files.provider.archive.archiver.ReadArchive import me.zhanghai.android.files.provider.common.AbstractPosixFileAttributes import me.zhanghai.android.files.provider.common.ByteString import me.zhanghai.android.files.provider.common.FileTimeParceler -import me.zhanghai.android.files.provider.common.PosixFileMode import me.zhanghai.android.files.provider.common.PosixFileModeBit import me.zhanghai.android.files.provider.common.PosixFileType import me.zhanghai.android.files.provider.common.PosixGroup import me.zhanghai.android.files.provider.common.PosixUser -import me.zhanghai.android.files.provider.common.posixFileType -import me.zhanghai.android.files.provider.common.toByteString -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.dump.DumpArchiveEntry -import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.threeten.bp.Instant @Parcelize internal class ArchiveFileAttributes( @@ -44,72 +36,16 @@ internal class ArchiveFileAttributes( fun entryName(): String = entryName companion object { - fun from(archiveFile: Path, entry: ArchiveEntry): ArchiveFileAttributes { - val lastModifiedTime = FileTime.from(Instant.ofEpochMilli(entry.lastModifiedDate.time)) - val lastAccessTime = when (entry) { - is DumpArchiveEntry -> FileTime.from(Instant.ofEpochMilli(entry.accessTime.time)) - is SevenZArchiveEntry -> - if (entry.hasAccessDate) { - FileTime.from(Instant.ofEpochMilli(entry.accessDate.time)) - } else { - lastModifiedTime - } - is TarArchiveEntry -> { - val atimeMillis = entry.getExtraPaxHeaderTimeMillis("atime") - if (atimeMillis != null) { - FileTime.from(Instant.ofEpochMilli(atimeMillis)) - } else { - lastModifiedTime - } - } - else -> lastModifiedTime - } - val creationTime = when (entry) { - is DumpArchiveEntry -> FileTime.from(Instant.ofEpochMilli(entry.creationTime.time)) - is SevenZArchiveEntry -> - if (entry.hasCreationDate) { - FileTime.from(Instant.ofEpochMilli(entry.creationDate.time)) - } else { - lastModifiedTime - } - is TarArchiveEntry -> { - val ctimeMillis = entry.getExtraPaxHeaderTimeMillis("ctime") - if (ctimeMillis != null) { - FileTime.from(Instant.ofEpochMilli(ctimeMillis)) - } else { - lastModifiedTime - } - } - else -> lastModifiedTime - } - val type = entry.posixFileType + fun from(archiveFile: Path, entry: ReadArchive.Entry): ArchiveFileAttributes { + val lastModifiedTime = entry.lastModifiedTime ?: FileTime.fromMillis(0) + val lastAccessTime = entry.lastAccessTime ?: lastModifiedTime + val creationTime = entry.creationTime ?: lastModifiedTime + val type = entry.type val size = entry.size val fileKey = ArchiveFileKey(archiveFile, entry.name) - val owner = when (entry) { - is DumpArchiveEntry -> PosixUser(entry.userId, null) - is TarArchiveEntry -> - @Suppress("DEPRECATION") - PosixUser(entry.userId, entry.userName?.toByteString()) - else -> null - } - val group = when (entry) { - is DumpArchiveEntry -> PosixGroup(entry.groupId, null) - is TarArchiveEntry -> - @Suppress("DEPRECATION") - PosixGroup(entry.groupId, entry.groupName?.toByteString()) - else -> null - } - val mode = when (entry) { - is DumpArchiveEntry -> PosixFileMode.fromInt(entry.mode) - is TarArchiveEntry -> PosixFileMode.fromInt(entry.mode) - is ZipArchiveEntry -> - if (entry.platform == ZipArchiveEntry.PLATFORM_UNIX) { - PosixFileMode.fromInt(entry.unixMode) - } else { - null - } - else -> null - } + val owner = entry.owner + val group = entry.group + val mode = entry.mode val seLinuxContext = null val entryName = entry.name return ArchiveFileAttributes( @@ -117,16 +53,5 @@ internal class ArchiveFileAttributes( mode, seLinuxContext, entryName ) } - - private fun TarArchiveEntry.getExtraPaxHeaderTimeMillis(name: String): Long? { - val timeString = getExtraPaxHeader(name) ?: return null - val timeSeconds = try { - timeString.toDouble() - } catch (e: NumberFormatException) { - e.printStackTrace() - return null - } - return (timeSeconds * 1000).toLong() - } } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt index b906dc50f..46252ddd1 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt @@ -8,11 +8,11 @@ package me.zhanghai.android.files.provider.archive import android.os.Parcel import android.os.Parcelable import java8.nio.file.Path +import me.zhanghai.android.files.provider.archive.archiver.ReadArchive import me.zhanghai.android.files.provider.common.ByteString import me.zhanghai.android.files.provider.common.ByteStringListPathCreator import me.zhanghai.android.files.provider.remote.RemoteFileSystemException import me.zhanghai.android.files.provider.root.RootableFileSystem -import org.apache.commons.compress.archivers.ArchiveEntry import java.io.IOException import java.io.InputStream @@ -39,7 +39,7 @@ internal class ArchiveFileSystem( get() = localFileSystem.archiveFile @Throws(IOException::class) - fun getEntryAsLocal(path: Path): ArchiveEntry = localFileSystem.getEntry(path) + fun getEntryAsLocal(path: Path): ReadArchive.Entry = localFileSystem.getEntry(path) @Throws(IOException::class) fun newInputStreamAsLocal(file: Path): InputStream = localFileSystem.newInputStream(file) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt index 0cca8a316..10180388a 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt @@ -16,11 +16,11 @@ import java8.nio.file.WatchService import java8.nio.file.attribute.UserPrincipalLookupService import java8.nio.file.spi.FileSystemProvider import me.zhanghai.android.files.provider.archive.archiver.ArchiveReader +import me.zhanghai.android.files.provider.archive.archiver.ReadArchive import me.zhanghai.android.files.provider.common.ByteString import me.zhanghai.android.files.provider.common.ByteStringBuilder import me.zhanghai.android.files.provider.common.ByteStringListPathCreator import me.zhanghai.android.files.provider.common.toByteString -import org.apache.commons.compress.archivers.ArchiveEntry import java.io.IOException import java.io.InputStream @@ -49,19 +49,19 @@ internal class LocalArchiveFileSystem( private var isRefreshNeeded = true - private var entries: Map? = null + private var entries: Map? = null private var tree: Map>? = null @Throws(IOException::class) - fun getEntry(path: Path): ArchiveEntry = + fun getEntry(path: Path): ReadArchive.Entry = synchronized(lock) { ensureEntriesLocked() getEntryLocked(path) } @Throws(IOException::class) - private fun getEntryLocked(path: Path): ArchiveEntry = + private fun getEntryLocked(path: Path): ReadArchive.Entry = synchronized(lock) { entries!![path] ?: throw NoSuchFileException(path.toString()) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveException.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveException.kt deleted file mode 100644 index 9b5c2ca2b..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveException.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright (c) 2018 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.archive.archiver - -//#ifdef NONFREE -import com.github.junrar.exception.RarException -//#endif -import org.apache.commons.compress.compressors.CompressorException -import java.io.IOException -import org.apache.commons.compress.archivers.ArchiveException as ApacheArchiveException - -class ArchiveException : IOException { - constructor(cause: ApacheArchiveException) : super(cause) - - constructor(cause: CompressorException) : super(cause) - -//#ifdef NONFREE - constructor(cause: RarException) : super(cause) -//#endif -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt index da0d63a8e..96b46e560 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt @@ -5,64 +5,38 @@ package me.zhanghai.android.files.provider.archive.archiver -import android.os.Build -import androidx.annotation.RequiresApi import androidx.preference.PreferenceManager import java8.nio.channels.SeekableByteChannel import java8.nio.charset.StandardCharsets -import java8.nio.file.AccessMode import java8.nio.file.NoSuchFileException import java8.nio.file.NotLinkException import java8.nio.file.Path import me.zhanghai.android.files.R -import me.zhanghai.android.files.compat.toJavaSeekableByteChannel -//#ifdef NONFREE -import me.zhanghai.android.files.nonfree.RarArchiveEntry -import me.zhanghai.android.files.nonfree.RarFile -//#endif import me.zhanghai.android.files.provider.common.DelegateForceableSeekableByteChannel import me.zhanghai.android.files.provider.common.DelegateInputStream import me.zhanghai.android.files.provider.common.DelegateNonForceableSeekableByteChannel import me.zhanghai.android.files.provider.common.ForceableChannel import me.zhanghai.android.files.provider.common.IsDirectoryException +import me.zhanghai.android.files.provider.common.PosixFileMode import me.zhanghai.android.files.provider.common.PosixFileType -import me.zhanghai.android.files.provider.common.checkAccess import me.zhanghai.android.files.provider.common.newByteChannel import me.zhanghai.android.files.provider.common.newInputStream -import me.zhanghai.android.files.provider.common.posixFileType -import me.zhanghai.android.files.provider.linux.isLinuxPath import me.zhanghai.android.files.provider.root.isRunningAsRoot import me.zhanghai.android.files.provider.root.rootContext import me.zhanghai.android.files.settings.Settings import me.zhanghai.android.files.util.valueCompat -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.ArchiveInputStream -import org.apache.commons.compress.archivers.ArchiveStreamFactory -import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry -import org.apache.commons.compress.archivers.sevenz.SevenZFile -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.compressors.CompressorException -import org.apache.commons.compress.compressors.CompressorStreamFactory -import java.io.BufferedInputStream import java.io.Closeable -import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream -import java.util.Date -import kotlin.reflect.KClass -import org.apache.commons.compress.archivers.ArchiveException as ApacheArchiveException +import java.nio.charset.Charset object ArchiveReader { - private val compressorStreamFactory = CompressorStreamFactory() - private val archiveStreamFactory = ArchiveStreamFactory() - @Throws(IOException::class) fun readEntries( file: Path, rootPath: Path - ): Pair, Map>> { - val entries = mutableMapOf() + ): Pair, Map>> { + val entries = mutableMapOf() val rawEntries = readEntries(file) for (entry in rawEntries) { var path = rootPath.resolve(entry.name) @@ -80,7 +54,7 @@ object ArchiveReader { } entries.getOrPut(path) { entry } } - entries.getOrPut(rootPath) { DirectoryArchiveEntry("") } + entries.getOrPut(rootPath) { createDirectoryEntry("") } val tree = mutableMapOf>() tree[rootPath] = mutableListOf() val paths = entries.keys.toList() @@ -96,235 +70,46 @@ object ArchiveReader { if (entries.containsKey(parentPath)) { break } - entries[parentPath] = DirectoryArchiveEntry(parentPath.toString()) + entries[parentPath] = createDirectoryEntry(parentPath.toString()) path = parentPath } } return entries to tree } + private fun createDirectoryEntry(name: String): ReadArchive.Entry { + require(!name.endsWith("/")) { "name $name should not end with a slash" } + return ReadArchive.Entry( + name, null, null, null, PosixFileType.DIRECTORY, 0, null, null, + PosixFileMode.DIRECTORY_DEFAULT, null + ) + } + @Throws(IOException::class) - private fun readEntries(file: Path): List { - val compressorType: String? - val archiveType = try { - file.newInputStream().buffered().use { inputStream -> - compressorType = try { - // inputStream must be buffered for markSupported(). - CompressorStreamFactory.detect(inputStream) - } catch (e: CompressorException) { - // Ignored. - null - } - val compressorInputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorInputStream(compressorType, inputStream) - .buffered() - } else { - inputStream - } - try { - // compressorInputStream must be buffered for markSupported(). - compressorInputStream.use { detectArchiveType(it) } - } catch (e: ApacheArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) + private fun readEntries(file: Path): List { + val charset = archiveFileNameCharset + val (archive, closeable) = openArchive(file) + return closeable.use { + buildList { + while (true) { + this += archive.readEntry(charset) ?: break } } - } catch (e: FileNotFoundException) { - file.checkAccess(AccessMode.READ) - throw NoSuchFileException(file.toString()).apply { initCause(e) } - } - val encoding = archiveFileNameEncoding - if (compressorType == null) { - when { - archiveType == ArchiveStreamFactory.ZIP && ZipFileCompat::class.isSupported(file) -> - return ZipFileCompat::class.create(file, encoding).use { it.entries.toList() } - archiveType == ArchiveStreamFactory.SEVEN_Z -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - throw IOException(UnsupportedOperationException("SevenZFile")) - } - return SevenZFile::class.create(file).use { it.entries.toList() } - } - //#ifdef NONFREE - archiveType == RarFile.RAR -> - return RarFile.create(file, encoding).use { it.entries.toList() } - //#endif - // Unnecessary, but teaches lint that compressorType != null below might be false. - else -> {} - } - } - return try { - file.newInputStream().buffered().use { inputStream -> - val compressorInputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorInputStream(compressorType, inputStream) - } else { - inputStream - } - compressorInputStream.use { - archiveStreamFactory.createArchiveInputStream( - archiveType, compressorInputStream, encoding - ).use { archiveInputStream -> - val entries = mutableListOf() - while (true) { - val entry = archiveInputStream.nextEntry ?: break - entries += entry - } - entries - } - } - } - } catch (e: FileNotFoundException) { - throw NoSuchFileException(file.toString()).apply { initCause(e) } - } catch (e: ApacheArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) } } - private val archiveFileNameEncoding: String - get() = - if (isRunningAsRoot) { - try { - val sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(rootContext) - val key = rootContext.getString(R.string.pref_key_archive_file_name_encoding) - val defaultValue = rootContext.getString( - R.string.pref_default_value_archive_file_name_encoding - ) - sharedPreferences.getString(key, defaultValue)!! - } catch (e: Exception) { - e.printStackTrace() - StandardCharsets.UTF_8.name() - } - } else { - Settings.ARCHIVE_FILE_NAME_ENCODING.valueCompat - } - @Throws(IOException::class) - fun newInputStream(file: Path, entry: ArchiveEntry): InputStream { + fun newInputStream(file: Path, entry: ReadArchive.Entry): InputStream { if (entry.isDirectory) { throw IsDirectoryException(file.toString()) } - val compressorType: String? - val archiveType = try { - file.newInputStream().buffered().use { inputStream -> - compressorType = try { - // inputStream must be buffered for markSupported(). - CompressorStreamFactory.detect(inputStream) - } catch (e: CompressorException) { - // Ignored. - null - } - val compressorInputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorInputStream(compressorType, inputStream) - .buffered() - } else { - inputStream - } - try { - // compressorInputStream must be buffered for markSupported(). - compressorInputStream.use { detectArchiveType(it) } - } catch (e: ApacheArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) - } - } - } catch (e: FileNotFoundException) { - file.checkAccess(AccessMode.READ) - throw NoSuchFileException(file.toString()).apply { initCause(e) } - } - val encoding = archiveFileNameEncoding - if (compressorType == null) { - when { - entry is ZipArchiveEntry && ZipFileCompat::class.isSupported(file) -> { - var successful = false - var zipFile: ZipFileCompat? = null - var zipEntryInputStream: InputStream? = null - return try { - zipFile = ZipFileCompat::class.create(file, encoding) - zipEntryInputStream = zipFile.getInputStream(entry) - ?: throw NoSuchFileException(file.toString()) - val inputStream = CloseableInputStream(zipEntryInputStream, zipFile) - successful = true - inputStream - } finally { - if (!successful) { - zipEntryInputStream?.close() - zipFile?.close() - } - } - } - entry is SevenZArchiveEntry -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - throw IOException(UnsupportedOperationException("SevenZFile")) - } - var successful = false - var sevenZFile: SevenZFile? = null - return try { - sevenZFile = SevenZFile::class.create(file) - var inputStream: InputStream? = null - while (true) { - val currentEntry = sevenZFile.nextEntry ?: break - if (currentEntry.name != entry.name) { - continue - } - inputStream = SevenZArchiveEntryInputStream(sevenZFile, currentEntry) - successful = true - break - } - inputStream ?: throw NoSuchFileException(file.toString()) - } finally { - if (!successful) { - sevenZFile?.close() - } - } - } -//#ifdef NONFREE - entry is RarArchiveEntry -> { - var successful = false - var rarFile: RarFile? = null - return try { - rarFile = RarFile.create(file, encoding) - var inputStream: InputStream? = null - while (true) { - val currentEntry = rarFile.nextEntry ?: break - if (currentEntry.name != entry.name) { - continue - } - inputStream = rarFile.getInputStream(currentEntry) - successful = true - break - } - inputStream ?: throw NoSuchFileException(file.toString()) - } finally { - if (!successful) { - rarFile?.close() - } - } - } -//#endif - // Unnecessary, but teaches lint that compressorType != null below might be false. - else -> {} - } - } + val charset = archiveFileNameCharset + val (archive, closeable) = openArchive(file) var successful = false - var inputStream: BufferedInputStream? = null - var compressorInputStream: InputStream? = null - var archiveInputStream: ArchiveInputStream? = null return try { - inputStream = file.newInputStream().buffered() - compressorInputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorInputStream(compressorType, inputStream) - } else { - inputStream - } - archiveInputStream = archiveStreamFactory.createArchiveInputStream( - archiveType, compressorInputStream, encoding - ) + var currentEntry: ReadArchive.Entry? = null while (true) { - val currentEntry = archiveInputStream.nextEntry ?: break + currentEntry = archive.readEntry(charset) ?: break if (currentEntry.name != entry.name) { continue } @@ -332,55 +117,50 @@ object ArchiveReader { break } if (successful) { - archiveInputStream + CloseableInputStream(archive.newDataInputStream(), closeable) } else { throw NoSuchFileException(file.toString()) } - } catch (e: FileNotFoundException) { - throw NoSuchFileException(file.toString()).apply { initCause(e) } - } catch (e: ApacheArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) } finally { if (!successful) { - archiveInputStream?.close() - compressorInputStream?.close() - inputStream?.close() + closeable.close() } } } - @Throws(ApacheArchiveException::class) - private fun detectArchiveType(inputStream: InputStream): String = -//#ifdef NONFREE + private fun openArchive(file: Path): Pair { + val channel = try { + CacheSizeSeekableByteChannel(file.newByteChannel()) + } catch (e: Exception) { + e.printStackTrace() + null + } + if (channel != null) { + var successful = false + try { + val archive = ReadArchive(channel) + successful = true + return archive to ArchiveCloseable(archive, channel) + } finally { + if (!successful) { + channel.close() + } + } + } + val inputStream = file.newInputStream() + var successful = false try { - RarFile.detect(inputStream) - } catch (e: IOException) { - throw ApacheArchiveException("RarFile.detect()", e) - } ?: -//#endif - ArchiveStreamFactory.detect(inputStream) - - private fun KClass.isSupported(file: Path): Boolean = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || file.isLinuxPath - - private fun KClass.create(file: Path, encoding: String?): ZipFileCompat = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ZipFileCompat( - CacheSizeSeekableByteChannel(file.newByteChannel()).toJavaSeekableByteChannel(), - encoding - ) - } else { - ZipFileCompat(file.toFile()) + val archive = ReadArchive(inputStream) + successful = true + return archive to ArchiveCloseable(archive, inputStream) + } finally { + if (!successful) { + inputStream.close() + } } + } - @RequiresApi(Build.VERSION_CODES.N) - private fun KClass.create(file: Path): SevenZFile = - SevenZFile(CacheSizeSeekableByteChannel(file.newByteChannel()).toJavaSeekableByteChannel()) - - // ZipFileCompat and SevenZFile call size() repeatedly, especially ZipFile.skipBytes(), so make - // it cached to improve performance. + // size() may be called repeatedly for ZIP and 7Z, so make it cached to improve performance. private fun CacheSizeSeekableByteChannel(channel: SeekableByteChannel): SeekableByteChannel = if (channel is ForceableChannel) { CacheSizeForceableSeekableByteChannel(channel) @@ -404,48 +184,37 @@ object ArchiveReader { override fun size(): Long = size } - @Throws(IOException::class) - fun readSymbolicLink(file: Path, entry: ArchiveEntry): String { - if (!isSymbolicLink(entry)) { - throw NotLinkException(file.toString()) - } - return if (entry is TarArchiveEntry) { - entry.linkName - } else { - newInputStream(file, entry).use { it.reader(StandardCharsets.UTF_8).readText() } - } - } - - private fun isSymbolicLink(entry: ArchiveEntry): Boolean = - entry.posixFileType == PosixFileType.SYMBOLIC_LINK - - private class DirectoryArchiveEntry(name: String) : ArchiveEntry { - init { - require(!name.endsWith("/")) { "name $name should not end with a slash" } - } - - private val name = "$name/" - - override fun getName(): String = name - - override fun getSize(): Long = 0 - - override fun isDirectory(): Boolean = true - - override fun getLastModifiedDate(): Date = Date(-1) - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true + private val archiveFileNameCharset: Charset + get() = + if (isRunningAsRoot) { + try { + val sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(rootContext) + val key = rootContext.getString(R.string.pref_key_archive_file_name_encoding) + val defaultValue = rootContext.getString( + R.string.pref_default_value_archive_file_name_encoding + ) + Charset.forName(sharedPreferences.getString(key, defaultValue)!!) + } catch (e: Exception) { + e.printStackTrace() + StandardCharsets.UTF_8 + } + } else { + Charset.forName(Settings.ARCHIVE_FILE_NAME_ENCODING.valueCompat) } - if (javaClass != other?.javaClass) { - return false + + private class ArchiveCloseable( + private val archive: ReadArchive, + private val closeable: Closeable + ) : Closeable { + override fun close() { + @Suppress("ConvertTryFinallyToUseCall") + try { + archive.close() + } finally { + closeable.close() } - other as DirectoryArchiveEntry - return name == other.name } - - override fun hashCode(): Int = name.hashCode() } private class CloseableInputStream( @@ -460,30 +229,11 @@ object ArchiveReader { } } - private class SevenZArchiveEntryInputStream( - private val file: SevenZFile, - private val entry: SevenZArchiveEntry - ) : InputStream() { - override fun available(): Int { - val size = entry.size - val read = file.statisticsForCurrentEntry - .uncompressedCount - val available = size - read - return available.coerceAtMost(Int.MAX_VALUE.toLong()).toInt() - } - - @Throws(IOException::class) - override fun read(): Int = file.read() - - @Throws(IOException::class) - override fun read(b: ByteArray): Int = file.read(b) - - @Throws(IOException::class) - override fun read(b: ByteArray, off: Int, len: Int): Int = file.read(b, off, len) - - @Throws(IOException::class) - override fun close() { - file.close() + @Throws(IOException::class) + fun readSymbolicLink(file: Path, entry: ReadArchive.Entry): String { + if (entry.type != PosixFileType.SYMBOLIC_LINK) { + throw NotLinkException(file.toString()) } + return entry.symbolicLinkTarget ?: "" } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveWriter.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveWriter.kt index f8a1e2958..3524f897b 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveWriter.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveWriter.kt @@ -5,224 +5,74 @@ package me.zhanghai.android.files.provider.archive.archiver -import android.os.Build import java8.nio.channels.SeekableByteChannel import java8.nio.file.LinkOption import java8.nio.file.Path import java8.nio.file.attribute.BasicFileAttributes -import me.zhanghai.android.files.compat.toJavaSeekableByteChannel import me.zhanghai.android.files.provider.common.PosixFileAttributes +import me.zhanghai.android.files.provider.common.PosixFileMode +import me.zhanghai.android.files.provider.common.PosixFileType import me.zhanghai.android.files.provider.common.copyTo import me.zhanghai.android.files.provider.common.getLastModifiedTime -import me.zhanghai.android.files.provider.common.isDirectory -import me.zhanghai.android.files.provider.common.isRegularFile import me.zhanghai.android.files.provider.common.newInputStream -import me.zhanghai.android.files.provider.common.newOutputStream import me.zhanghai.android.files.provider.common.readAttributes import me.zhanghai.android.files.provider.common.readSymbolicLinkByteString import me.zhanghai.android.files.provider.common.size -import me.zhanghai.android.files.provider.common.toInt -import me.zhanghai.android.files.util.lazyReflectedField -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.ArchiveOutputStream -import org.apache.commons.compress.archivers.ArchiveStreamFactory -import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.tar.TarConstants -import org.apache.commons.compress.archivers.zip.UnixStat -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.compressors.CompressorException -import org.apache.commons.compress.compressors.CompressorStreamFactory import java.io.Closeable -import java.io.File import java.io.IOException -import java.io.OutputStream -class ArchiveWriter( - archiveType: String, - compressorType: String?, - channel: SeekableByteChannel +class ArchiveWriter @Throws(IOException::class) constructor( + channel: SeekableByteChannel, + format: Int, + filter: Int, + password: String? ) : Closeable { - private val archiveOutputStream: ArchiveOutputStream - - init { - when (archiveType) { - ArchiveStreamFactory.SEVEN_Z -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - throw IOException(UnsupportedOperationException("SevenZOutputFile")) - } - archiveOutputStream = SevenZArchiveOutputStream( - SevenZOutputFile(channel.toJavaSeekableByteChannel()) - ) - } - else -> { - var successful = false - var outputStream: OutputStream? = null - var compressorOutputStream: OutputStream? = null - try { - outputStream = channel.newOutputStream().buffered() - compressorOutputStream = if (compressorType != null) { - compressorStreamFactory.createCompressorOutputStream( - compressorType, outputStream - ) - } else { - outputStream - } - // Use the platform default encoding (which is UTF-8) instead of the user-set - // one, because that one is for reading archives instead of creating. - archiveOutputStream = archiveStreamFactory.createArchiveOutputStream( - archiveType, compressorOutputStream - ) - successful = true - } catch (e: org.apache.commons.compress.archivers.ArchiveException) { - throw ArchiveException(e) - } catch (e: CompressorException) { - throw ArchiveException(e) - } finally { - if (!successful) { - compressorOutputStream?.close() - outputStream?.close() - } - } - } - } - } + private val archive = WriteArchive(channel, format, filter, password) @Throws(IOException::class) fun write(file: Path, entryName: Path, intervalMillis: Long, listener: ((Long) -> Unit)?) { - val entry = archiveOutputStream.createArchiveEntry(PathFile(file), entryName.toString()) + val name = entryName.toString() + val lastModifiedTime = file.getLastModifiedTime(LinkOption.NOFOLLOW_LINKS) + val lastAccessTime = null + val creationTime = null val attributes = file.readAttributes( BasicFileAttributes::class.java, LinkOption.NOFOLLOW_LINKS ) - val writeData = when { - attributes.isRegularFile -> true - attributes.isDirectory -> false - attributes.isSymbolicLink -> - when (entry) { - is ZipArchiveEntry -> { - entry.unixMode = UnixStat.LINK_FLAG or UnixStat.DEFAULT_LINK_PERM - true - } - is TarArchiveEntry -> { - tarArchiveEntryLinkFlagsField.setByte(entry, TarConstants.LF_SYMLINK) - entry.linkName = file.readSymbolicLinkByteString().toString() - false - } - else -> throw IOException(UnsupportedOperationException("symbolic link")) - } - else -> throw IOException(UnsupportedOperationException("type")) + val type = when { + attributes is PosixFileAttributes -> attributes.type() + attributes.isDirectory -> PosixFileType.DIRECTORY + attributes.isSymbolicLink -> PosixFileType.SYMBOLIC_LINK + else -> PosixFileType.REGULAR_FILE } - if (entry is TarArchiveEntry && attributes is PosixFileAttributes) { - attributes.mode()?.let { entry.mode = it.toInt() } - val owner = attributes.owner() - if (owner != null) { - entry.userId = owner.id - owner.name?.let { entry.userName = it } - } - val group = attributes.group() - if (group != null) { - entry.groupId = group.id - group.name?.let { entry.groupName = it } - } + val size = file.size(LinkOption.NOFOLLOW_LINKS) + val posixAttributes = attributes as? PosixFileAttributes + val owner = posixAttributes?.owner() + val group = posixAttributes?.group() + val mode = posixAttributes?.mode() ?: when { + attributes.isDirectory -> PosixFileMode.DIRECTORY_DEFAULT + attributes.isSymbolicLink -> PosixFileMode.SYMBOLIC_LINK_DEFAULT + else -> PosixFileMode.FILE_DEFAULT } - archiveOutputStream.putArchiveEntry(entry) - var isListenerNotified = false - if (writeData) { - if (attributes.isSymbolicLink) { - val target = file.readSymbolicLinkByteString().borrowBytes() - archiveOutputStream.write(target) - } else { - file.newInputStream(LinkOption.NOFOLLOW_LINKS).use { inputStream -> - inputStream.copyTo(archiveOutputStream, intervalMillis, listener) - } - isListenerNotified = true - } + val symbolicLinkTarget = if (attributes.isSymbolicLink) { + file.readSymbolicLinkByteString().toString() + } else { + null } - archiveOutputStream.closeArchiveEntry() - if (!isListenerNotified) { + archive.Entry( + name, lastModifiedTime, lastAccessTime, creationTime, type, size, owner, group, mode, + symbolicLinkTarget + ).use { archive.writeEntry(it) } + if (type == PosixFileType.REGULAR_FILE) { + file.newInputStream(LinkOption.NOFOLLOW_LINKS).use { inputStream -> + inputStream.copyTo(archive.newDataOutputStream(), intervalMillis, listener) + } + } else { listener?.invoke(attributes.size()) } } @Throws(IOException::class) override fun close() { - archiveOutputStream.finish() - archiveOutputStream.close() - } - - private class SevenZArchiveOutputStream( - private val file: SevenZOutputFile - ) : ArchiveOutputStream() { - @Throws(IOException::class) - override fun createArchiveEntry(file: File, entryName: String): ArchiveEntry = - this.file.createArchiveEntry(file, entryName) - - @Throws(IOException::class) - override fun putArchiveEntry(entry: ArchiveEntry) { - file.putArchiveEntry(entry) - } - - @Throws(IOException::class) - override fun write(b: Int) { - file.write(b) - } - - @Throws(IOException::class) - override fun write(b: ByteArray) { - file.write(b) - } - - @Throws(IOException::class) - override fun write(b: ByteArray, off: Int, len: Int) { - file.write(b, off, len) - } - - @Throws(IOException::class) - override fun closeArchiveEntry() { - file.closeArchiveEntry() - } - - @Throws(IOException::class) - override fun finish() { - file.finish() - } - - @Throws(IOException::class) - override fun close() { - file.close() - } - } - - // {@link ArchiveOutputStream#createArchiveEntry(File, String)} doesn't actually need a real - // file. - private class PathFile(private val path: Path) : File(path.toString()) { - override fun isDirectory(): Boolean = path.isDirectory(LinkOption.NOFOLLOW_LINKS) - - override fun isFile(): Boolean = path.isRegularFile(LinkOption.NOFOLLOW_LINKS) - - override fun lastModified(): Long = - try { - path.getLastModifiedTime(LinkOption.NOFOLLOW_LINKS).toMillis() - } catch (e: IOException) { - e.printStackTrace() - 0 - } - - override fun length(): Long = - try { - path.size(LinkOption.NOFOLLOW_LINKS) - } catch (e: IOException) { - e.printStackTrace() - 0 - } - } - - companion object { - private val compressorStreamFactory = CompressorStreamFactory() - private val archiveStreamFactory = ArchiveStreamFactory() - - private val tarArchiveEntryLinkFlagsField by lazyReflectedField( - TarArchiveEntry::class.java, "linkFlag" - ) + archive.close() } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt new file mode 100644 index 000000000..a6aa0d924 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.provider.archive.archiver + +import android.system.OsConstants +import java8.nio.channels.SeekableByteChannel +import java8.nio.charset.StandardCharsets +import java8.nio.file.attribute.FileTime +import me.zhanghai.android.files.provider.common.PosixFileMode +import me.zhanghai.android.files.provider.common.PosixFileModeBit +import me.zhanghai.android.files.provider.common.PosixFileType +import me.zhanghai.android.files.provider.common.PosixGroup +import me.zhanghai.android.files.provider.common.PosixUser +import me.zhanghai.android.files.provider.common.toByteString +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveException +import org.threeten.bp.Instant +import java.io.Closeable +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.charset.Charset + +class ReadArchive : Closeable { + private val archive = Archive.readNew() + + @Throws(ArchiveException::class) + constructor(inputStream: InputStream) { + var successful = false + try { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + Archive.readSetCallbackData(archive, null) + val buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE) + Archive.readSetReadCallback(archive) { _, _ -> + buffer.clear() + val bytesRead = try { + inputStream.read(buffer.array()) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.read", e) + } + if (bytesRead != -1) { + buffer.limit(bytesRead) + buffer + } else { + null + } + } + Archive.readSetSkipCallback(archive) { _, _, request -> + try { + inputStream.skip(request) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.skip", e) + } + } + Archive.readOpen1(archive) + successful = true + } finally { + if (!successful) { + close() + } + } + } + + @Throws(ArchiveException::class) + constructor(channel: SeekableByteChannel) { + var successful = false + try { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + Archive.readSupportFilterAll(archive) + Archive.readSupportFormatAll(archive) + Archive.readSetCallbackData(archive, null) + val buffer = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE) + Archive.readSetReadCallback(archive) { _, _ -> + buffer.clear() + val bytesRead = try { + channel.read(buffer) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.read", e) + } + if (bytesRead != -1) { + buffer.flip() + buffer + } else { + null + } + } + Archive.readSetSkipCallback(archive) { _, _, request -> + try { + channel.position(channel.position() + request) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e) + } + request + } + Archive.readSetSeekCallback(archive) { _, _, offset, whence -> + val newPosition: Long + try { + newPosition = when (whence) { + OsConstants.SEEK_SET -> offset + OsConstants.SEEK_CUR -> channel.position() + offset + OsConstants.SEEK_END -> channel.size() + offset + else -> throw ArchiveException( + Archive.ERRNO_FATAL, + "Unknown whence $whence" + ) + } + channel.position(newPosition) + } catch (e: IOException) { + throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e) + } + newPosition + } + Archive.readOpen1(archive) + successful = true + } finally { + if (!successful) { + close() + } + } + } + + @Throws(ArchiveException::class) + fun readEntry(charset: Charset): Entry? { + val entry = Archive.readNextHeader(archive) + if (entry == 0L) { + return null + } + val name = + getEntryString(ArchiveEntry.pathnameUtf8(entry), ArchiveEntry.pathname(entry), charset) + ?: throw ArchiveException( + Archive.ERRNO_FATAL, "pathname == null && pathnameUtf8 == null" + ) + val stat = ArchiveEntry.stat(entry) + val lastModifiedTime = if (ArchiveEntry.mtimeIsSet(entry)) { + FileTime.from( + Instant.ofEpochSecond(stat.stMtim.tvSec, stat.stMtim.tvNsec) + ) + } else { + null + } + val lastAccessTime = if (ArchiveEntry.atimeIsSet(entry)) { + FileTime.from( + Instant.ofEpochSecond(stat.stAtim.tvSec, stat.stAtim.tvNsec) + ) + } else { + null + } + val creationTime = if (ArchiveEntry.birthtimeIsSet(entry)) { + FileTime.from( + Instant.ofEpochSecond( + ArchiveEntry.birthtime(entry), ArchiveEntry.birthtimeNsec(entry) + ) + ) + } else { + null + } + val type = PosixFileType.fromMode(stat.stMode) + val size = stat.stSize + // TODO: There's no way to know if UID/GID is unset or root. + val owner = PosixUser( + stat.stUid, getEntryString( + ArchiveEntry.unameUtf8(entry), ArchiveEntry.uname(entry), charset + )?.toByteString() + ) + val group = PosixGroup( + stat.stGid, getEntryString( + ArchiveEntry.gnameUtf8(entry), ArchiveEntry.gname(entry), charset + )?.toByteString() + ) + val mode = PosixFileMode.fromInt(stat.stMode) + val symbolicLinkTarget = + getEntryString(ArchiveEntry.symlinkUtf8(entry), ArchiveEntry.symlink(entry), charset) + return Entry( + name, lastModifiedTime, lastAccessTime, creationTime, type, size, owner, group, mode, + symbolicLinkTarget + ) + } + + private fun getEntryString(stringUtf8: String?, string: ByteArray?, charset: Charset): String? = + stringUtf8 ?: string?.toString(charset) + + fun hasEncryptedEntries(): Boolean? { + val hasEncryptedEntries = Archive.readHasEncryptedEntries(archive) + return when { + hasEncryptedEntries > 0 -> true + hasEncryptedEntries == 0 -> false + else -> null + } + } + + @Throws(ArchiveException::class) + fun newDataInputStream(): InputStream = DataInputStream() + + @Throws(ArchiveException::class) + fun addPassword(password: String) { + Archive.readAddPassphrase(archive, password.toByteArray()) + } + + @Throws(ArchiveException::class) + override fun close() { + Archive.readFree(archive) + } + + class Entry( + val name: String, + val lastModifiedTime: FileTime?, + val lastAccessTime: FileTime?, + val creationTime: FileTime?, + val type: PosixFileType, + val size: Long, + val owner: PosixUser?, + val group: PosixGroup?, + val mode: Set, + val symbolicLinkTarget: String? + ) { + val isDirectory: Boolean + get() = type == PosixFileType.DIRECTORY + } + + private inner class DataInputStream : InputStream() { + private val oneByteBuffer = ByteBuffer.allocateDirect(1) + + @Throws(IOException::class) + override fun read(): Int { + read(oneByteBuffer) + return if (oneByteBuffer.hasRemaining()) oneByteBuffer.get().toUByte().toInt() else -1 + } + + @Throws(IOException::class) + override fun read(b: ByteArray, off: Int, len: Int): Int { + val buffer = ByteBuffer.wrap(b, off, len) + read(buffer) + return if (buffer.hasRemaining()) buffer.remaining() else -1 + } + + @Throws(IOException::class) + private fun read(buffer: ByteBuffer) { + buffer.clear() + Archive.readData(archive, buffer) + buffer.flip() + } + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt new file mode 100644 index 000000000..42781ef8a --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.provider.archive.archiver + +import java8.nio.channels.SeekableByteChannel +import java8.nio.charset.StandardCharsets +import java8.nio.file.attribute.FileTime +import me.zhanghai.android.files.provider.common.PosixFileModeBit +import me.zhanghai.android.files.provider.common.PosixFileType +import me.zhanghai.android.files.provider.common.PosixGroup +import me.zhanghai.android.files.provider.common.PosixUser +import me.zhanghai.android.files.provider.common.toInt +import me.zhanghai.android.libarchive.Archive +import me.zhanghai.android.libarchive.ArchiveEntry +import me.zhanghai.android.libarchive.ArchiveException +import java.io.Closeable +import java.io.IOException +import java.io.OutputStream +import java.nio.ByteBuffer + +class WriteArchive @Throws(ArchiveException::class) constructor( + channel: SeekableByteChannel, + format: Int, + filter: Int, + password: String? +) : Closeable { + private val archive = Archive.writeNew() + + init { + var successful = false + try { + Archive.writeSetBytesPerBlock(archive, DEFAULT_BUFFER_SIZE) + Archive.writeSetBytesInLastBlock(archive, 1) + Archive.writeSetFormat(archive, format) + Archive.writeAddFilter(archive, filter) + if (password != null) { + Archive.writeSetPassphrase(archive, password.toByteArray()) + } + Archive.writeOpen( + archive, null, null, { _, _, buffer -> channel.write(buffer) }, null + ) + successful = true + } finally { + if (!successful) { + close() + } + } + } + + @Throws(ArchiveException::class) + fun writeEntry(entry: Entry) { + Archive.writeHeader(archive, entry.entry) + } + + @Throws(ArchiveException::class) + fun newDataOutputStream(): OutputStream = DataOutputStream() + + @Throws(ArchiveException::class) + override fun close() { + Archive.writeFree(archive) + } + + inner class Entry( + name: String, + lastModifiedTime: FileTime?, + lastAccessTime: FileTime?, + creationTime: FileTime?, + type: PosixFileType, + size: Long, + owner: PosixUser?, + group: PosixGroup?, + mode: Set, + symbolicLinkTarget: String? + ) : Closeable { + internal val entry = ArchiveEntry.new2(archive) + + init { + Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) + ArchiveEntry.setPathname(entry, name.toByteArray()) + if (lastModifiedTime != null) { + val lastModifiedTimeInstant = lastModifiedTime.toInstant() + ArchiveEntry.setMtime( + entry, lastModifiedTimeInstant.epochSecond, + lastModifiedTimeInstant.nano.toLong() + ) + } + if (lastAccessTime != null) { + val lastAccessTimeInstant = lastAccessTime.toInstant() + ArchiveEntry.setAtime( + entry, lastAccessTimeInstant.epochSecond, lastAccessTimeInstant.nano.toLong() + ) + } + if (creationTime != null) { + val creationTimeInstant = creationTime.toInstant() + ArchiveEntry.setBirthtime( + entry, creationTimeInstant.epochSecond, creationTimeInstant.nano.toLong() + ) + } + ArchiveEntry.setFiletype(entry, type.mode) + ArchiveEntry.setSize(entry, size) + if (owner != null) { + ArchiveEntry.setUid(entry, owner.id.toLong()) + val ownerName = owner.name + if (ownerName != null) { + ArchiveEntry.setUname(entry, ownerName.toByteArray()) + } + } + if (group != null) { + ArchiveEntry.setGid(entry, group.id.toLong()) + val groupName = group.name + if (groupName != null) { + ArchiveEntry.setGname(entry, groupName.toByteArray()) + } + } + ArchiveEntry.setPerm(entry, mode.toInt()) + if (symbolicLinkTarget != null) { + ArchiveEntry.setSymlink(entry, symbolicLinkTarget.toByteArray()) + } + } + + override fun close() { + ArchiveEntry.free(entry) + } + } + + private inner class DataOutputStream : OutputStream() { + private val oneByteBuffer = ByteBuffer.allocateDirect(1) + + @Throws(IOException::class) + override fun write(b: Int) { + oneByteBuffer.clear() + oneByteBuffer.put(b.toByte()) + Archive.writeData(archive, oneByteBuffer) + } + + @Throws(IOException::class) + override fun write(b: ByteArray, off: Int, len: Int) { + val buffer = ByteBuffer.wrap(b, off, len) + while (buffer.hasRemaining()) { + Archive.writeData(archive, buffer) + } + } + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ZipFileCompat.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ZipFileCompat.kt deleted file mode 100644 index 45265ec7e..000000000 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ZipFileCompat.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2018 Hai Zhang - * All Rights Reserved. - */ - -package me.zhanghai.android.files.provider.archive.archiver - -import android.annotation.SuppressLint -import android.os.Build -import androidx.annotation.RequiresApi -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipFile -import java.io.Closeable -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.nio.channels.SeekableByteChannel -import java.util.Enumeration -import java.util.zip.ZipEntry -import java.util.zip.ZipException -import java.util.zip.ZipFile as JavaZipFile - -internal class ZipFileCompat : Closeable { - @RequiresApi(Build.VERSION_CODES.N) - private val zipFile: ZipFile? - private val javaZipFile: JavaZipFile? - - @RequiresApi(Build.VERSION_CODES.N) - constructor(channel: SeekableByteChannel, encoding: String?) { - zipFile = ZipFile(channel, encoding) - javaZipFile = null - } - - constructor(file: File) { - @SuppressLint("NewApi") - zipFile = null - javaZipFile = JavaZipFile(file) - } - - val entries: Enumeration - get() = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - zipFile!!.entries - } else { - val entries = javaZipFile!!.entries() - object : Enumeration { - override fun hasMoreElements(): Boolean = entries.hasMoreElements() - - override fun nextElement(): ZipArchiveEntry { - val entry = entries.nextElement() - return try { - ZipArchiveEntry(entry) - } catch (e: ZipException) { - e.printStackTrace() - UnparseableExtraZipArchiveEntry(entry) - } - } - } - } - - @Throws(IOException::class) - fun getInputStream(entry: ZipArchiveEntry): InputStream? = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - zipFile!!.getInputStream(entry) - } else { - javaZipFile!!.getInputStream(entry) - } - - @Throws(IOException::class) - override fun close() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - zipFile!!.close() - } else { - javaZipFile!!.close() - } - } - - private class UnparseableExtraZipArchiveEntry(entry: ZipEntry) : ZipArchiveEntry(entry.name) { - init { - time = entry.time - setExtra() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - lastModifiedTime = entry.lastModifiedTime - lastAccessTime = entry.lastAccessTime - creationTime = entry.creationTime - } - val crc = entry.crc - if (crc in 0..0xFFFFFFFFL) { - setCrc(entry.crc) - } - val size = entry.size - if (size >= 0) { - setSize(size) - } - compressedSize = entry.compressedSize - method = entry.method - comment = entry.comment - } - } -} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileMode.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileMode.kt index b4c4b8434..bbf3e2c1d 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileMode.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileMode.kt @@ -27,12 +27,25 @@ enum class PosixFileModeBit { } object PosixFileMode { + val CREATE_DIRECTORY_DEFAULT = fromInt( + OsConstants.S_IRWXU or OsConstants.S_IRWXG or OsConstants.S_IRWXO + ) + val CREATE_FILE_DEFAULT = fromInt( - OsConstants.S_IRUSR or OsConstants.S_IWUSR or OsConstants.S_IRGRP or OsConstants.S_IWGRP - or OsConstants.S_IROTH or OsConstants.S_IWOTH + OsConstants.S_IRUSR or OsConstants.S_IWUSR or OsConstants.S_IRGRP or OsConstants.S_IWGRP or + OsConstants.S_IROTH or OsConstants.S_IWOTH ) - val CREATE_DIRECTORY_DEFAULT = fromInt( + val DIRECTORY_DEFAULT = fromInt( + OsConstants.S_IRWXU or OsConstants.S_IRGRP or OsConstants.S_IXGRP or + OsConstants.S_IROTH or OsConstants.S_IXOTH + ) + + val FILE_DEFAULT = fromInt( + OsConstants.S_IRUSR or OsConstants.S_IWUSR or OsConstants.S_IRGRP or OsConstants.S_IROTH + ) + + val SYMBOLIC_LINK_DEFAULT = fromInt( OsConstants.S_IRWXU or OsConstants.S_IRWXG or OsConstants.S_IRWXO ) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileType.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileType.kt index eec607f96..faf405c27 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileType.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/PosixFileType.kt @@ -7,21 +7,17 @@ package me.zhanghai.android.files.provider.common import android.system.OsConstants import java8.nio.file.attribute.BasicFileAttributes -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.dump.DumpArchiveEntry -import org.apache.commons.compress.archivers.tar.TarArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry // https://www.gnu.org/software/libc/manual/html_node/Testing-File-Type.html -enum class PosixFileType { - UNKNOWN, - DIRECTORY, - CHARACTER_DEVICE, - BLOCK_DEVICE, - REGULAR_FILE, - FIFO, - SYMBOLIC_LINK, - SOCKET; +enum class PosixFileType(val mode: Int) { + UNKNOWN(0), + DIRECTORY(OsConstants.S_IFDIR), + CHARACTER_DEVICE(OsConstants.S_IFCHR), + BLOCK_DEVICE(OsConstants.S_IFBLK), + REGULAR_FILE(OsConstants.S_IFREG), + FIFO(OsConstants.S_IFIFO), + SYMBOLIC_LINK(OsConstants.S_IFLNK), + SOCKET((OsConstants.S_IFSOCK)); companion object { fun fromMode(mode: Int): PosixFileType = @@ -38,49 +34,6 @@ enum class PosixFileType { } } -val ArchiveEntry.posixFileType: PosixFileType - get() = - when (this) { - is DumpArchiveEntry -> posixFileType - is TarArchiveEntry -> posixFileType - is ZipArchiveEntry -> posixFileType - else -> if (isDirectory) PosixFileType.DIRECTORY else PosixFileType.REGULAR_FILE - } - -private val DumpArchiveEntry.posixFileType: PosixFileType - get() = - when (type) { - DumpArchiveEntry.TYPE.SOCKET -> PosixFileType.SOCKET - DumpArchiveEntry.TYPE.LINK -> PosixFileType.SYMBOLIC_LINK - DumpArchiveEntry.TYPE.FILE -> PosixFileType.REGULAR_FILE - DumpArchiveEntry.TYPE.BLKDEV -> PosixFileType.BLOCK_DEVICE - DumpArchiveEntry.TYPE.DIRECTORY -> PosixFileType.DIRECTORY - DumpArchiveEntry.TYPE.CHRDEV -> PosixFileType.CHARACTER_DEVICE - DumpArchiveEntry.TYPE.FIFO -> PosixFileType.FIFO - DumpArchiveEntry.TYPE.WHITEOUT, DumpArchiveEntry.TYPE.UNKNOWN -> PosixFileType.UNKNOWN - else -> PosixFileType.UNKNOWN - } - -private val TarArchiveEntry.posixFileType: PosixFileType - get() = - when { - isDirectory -> PosixFileType.DIRECTORY - isFile -> PosixFileType.REGULAR_FILE - isSymbolicLink -> PosixFileType.SYMBOLIC_LINK - isCharacterDevice -> PosixFileType.CHARACTER_DEVICE - isBlockDevice -> PosixFileType.BLOCK_DEVICE - isFIFO -> PosixFileType.FIFO - else -> PosixFileType.UNKNOWN - } - -private val ZipArchiveEntry.posixFileType: PosixFileType - get() = - when { - isDirectory -> PosixFileType.DIRECTORY - isUnixSymlink -> PosixFileType.SYMBOLIC_LINK - else -> PosixFileType.REGULAR_FILE - } - val BasicFileAttributes.posixFileType: PosixFileType get() = when (this) { diff --git a/app/src/main/res/raw/licenses.xml b/app/src/main/res/raw/licenses.xml index fbbe26496..85d178dd0 100644 --- a/app/src/main/res/raw/licenses.xml +++ b/app/src/main/res/raw/licenses.xml @@ -154,6 +154,48 @@ Apache Software License 2.0 + + libarchive-android + https://github.com/zhanghai/libarchive-android + Copyright 2023 Google LLC + Apache Software License 2.0 + + + + libarchive + https://github.com/libarchive/libarchive + Copyright 2003 Tim Kientzle + BSD 2-Clause License + + + + bzip2 + https://gitlab.com/bzip2/bzip2 + Copyright 1996 Julian Seward + BSD 3-Clause License + + + + lz4 + https://github.com/lz4/lz4 + Copyright 2011 Yann Collet + BSD 2-Clause License + + + + zstd + https://github.com/facebook/zstd + Copyright Meta Platforms, Inc. and affiliates + BSD 3-Clause License + + + + mbedtls + https://github.com/Mbed-TLS/mbedtls + Copyright The Mbed TLS Contributors + Apache Software License 2.0 + + libselinux-android https://github.com/zhanghai/libselinux-android @@ -182,13 +224,6 @@ GNU General Public License 2.0 - - Apache Commons Compress - https://commons.apache.org/proper/commons-compress/ - Copyright 2002 The Apache Software Foundation - Apache Software License 2.0 - - Apache FtpServer https://mina.apache.org/ftpserver-project/index.html From 4ed9fb5aca9d0de816153b58554aca26ac57340b Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 10 Sep 2023 01:16:28 -0700 Subject: [PATCH 135/326] [Fix] Ignore non-directory root path entry in an archive. --- .../android/files/provider/archive/archiver/ArchiveReader.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt index 96b46e560..352f20610 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt @@ -51,6 +51,11 @@ object ArchiveReader { // Don't allow a path to become the root path only after normalization. continue } + } else { + if (!entry.isDirectory) { + // Ignore a root path that's not a directory + continue + } } entries.getOrPut(path) { entry } } From bf5e720cfde1dbb34df83ec160e02770a875957a Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 10 Sep 2023 03:45:31 -0700 Subject: [PATCH 136/326] [Feature] Request notification permission and target Android 13. Fixes: #998 --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 1 + .../files/app/BackgroundActivityStarter.kt | 3 - .../files/filelist/FileListFragment.kt | 91 ++++++++++++++++--- .../files/filelist/FileListViewModel.kt | 7 ++ ...issionInSettingsRationaleDialogFragment.kt | 39 ++++++++ ...cationPermissionRationaleDialogFragment.kt | 39 ++++++++ .../util/ForegroundNotificationManager.kt | 3 - app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values-zh-rTW/strings.xml | 4 +- app/src/main/res/values/strings.xml | 6 +- 11 files changed, 177 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt diff --git a/app/build.gradle b/app/build.gradle index 83cdd996a..258a91c06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,7 +34,7 @@ android { minSdk 21 // Not supporting notification runtime permission yet. //noinspection OldTargetApi - targetSdk 32 + targetSdk 33 versionCode 33 versionName '1.6.1' resValue 'string', 'app_version', versionName + ' (' + versionCode + ')' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b811860b6..87e66a657 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + diff --git a/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt b/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt index 1637a4ea0..3f3392ca4 100644 --- a/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt +++ b/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt @@ -5,7 +5,6 @@ package me.zhanghai.android.files.app -import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -52,8 +51,6 @@ object BackgroundActivityStarter { Lifecycle.State.STARTED ) - // TODO: Add POST_NOTIFICATIONS permission when targeting API 33. - @SuppressLint("MissingPermission") private fun notifyStartActivity( intent: Intent, title: CharSequence, diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index 2443724e4..1fdbf5d4b 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -124,6 +124,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. CreateFileDialogFragment.Listener, CreateDirectoryDialogFragment.Listener, NavigateToPathDialogFragment.Listener, NavigationFragment.Listener, ShowRequestAllFilesAccessRationaleDialogFragment.Listener, + ShowRequestNotificationPermissionRationaleDialogFragment.Listener, + ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.Listener, ShowRequestStoragePermissionRationaleDialogFragment.Listener, ShowRequestStoragePermissionInSettingsRationaleDialogFragment.Listener { private val requestAllFilesAccessLauncher = registerForActivityResult( @@ -133,9 +135,18 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. ActivityResultContracts.RequestPermission(), this::onRequestStoragePermissionResult ) private val requestStoragePermissionInSettingsLauncher = registerForActivityResult( - RequestStoragePermissionInSettingsContract(), + RequestPermissionInSettingsContract(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), this::onRequestStoragePermissionInSettingsResult ) + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private val requestNotificationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), this::onRequestNotificationPermissionResult + ) + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private val requestNotificationPermissionInSettingsLauncher = registerForActivityResult( + RequestPermissionInSettingsContract(android.Manifest.permission.POST_NOTIFICATIONS), + this::onRequestNotificationPermissionInSettingsResult + ) private val args by args() private val argsPath by lazy { args.intent.extraPath } @@ -320,7 +331,9 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. override fun onResume() { super.onResume() - ensureStorageAccess() + if (ensureStorageAccess()) { + ensureNotificationPermission() + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -1308,18 +1321,20 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. binding.drawerLayout?.closeDrawer(GravityCompat.START) } - private fun ensureStorageAccess() { + private fun ensureStorageAccess(): Boolean { if (viewModel.isStorageAccessRequested) { - return + return true } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()) { ShowRequestAllFilesAccessRationaleDialogFragment.show(this) viewModel.isStorageAccessRequested = true + return false } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { + if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != + PackageManager.PERMISSION_GRANTED + ) { if (shouldShowRequestPermissionRationale( android.Manifest.permission.WRITE_EXTERNAL_STORAGE )) { @@ -1328,8 +1343,10 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. requestStoragePermission() } viewModel.isStorageAccessRequested = true + return false } } + return true } override fun requestAllFilesAccess() { @@ -1352,8 +1369,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. viewModel.isStorageAccessRequested = false refresh() } else if (!shouldShowRequestPermissionRationale( - android.Manifest.permission.WRITE_EXTERNAL_STORAGE - )) { + android.Manifest.permission.WRITE_EXTERNAL_STORAGE + )) { ShowRequestStoragePermissionInSettingsRationaleDialogFragment.show(this) } } @@ -1369,6 +1386,57 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. } } + private fun ensureNotificationPermission(): Boolean { + if (viewModel.isNotificationPermissionRequested) { + return true + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED) { + if (shouldShowRequestPermissionRationale( + android.Manifest.permission.POST_NOTIFICATIONS + )) { + ShowRequestNotificationPermissionRationaleDialogFragment.show(this) + } else { + requestNotificationPermission() + } + viewModel.isNotificationPermissionRequested = true + return false + } + } + return true + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun requestNotificationPermission() { + requestNotificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun onRequestNotificationPermissionResult(isGranted: Boolean) { + if (isGranted) { + viewModel.isNotificationPermissionRequested = false + refresh() + } else if (!shouldShowRequestPermissionRationale( + android.Manifest.permission.POST_NOTIFICATIONS + )) { + ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.show(this) + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun requestNotificationPermissionInSettings() { + requestNotificationPermissionInSettingsLauncher.launch(Unit) + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun onRequestNotificationPermissionInSettingsResult(isGranted: Boolean) { + if (isGranted) { + viewModel.isNotificationPermissionRequested = false + refresh() + } + } + companion object { private const val ACTION_VIEW_DOWNLOADS = "me.zhanghai.android.files.intent.action.VIEW_DOWNLOADS" @@ -1389,7 +1457,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. Environment.isExternalStorageManager() } - private class RequestStoragePermissionInSettingsContract + private class RequestPermissionInSettingsContract(private val permissionName: String) : ActivityResultContract() { override fun createIntent(context: Context, input: Unit): Intent = Intent( @@ -1398,9 +1466,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. ) override fun parseResult(resultCode: Int, intent: Intent?): Boolean = - application.checkSelfPermissionCompat( - android.Manifest.permission.WRITE_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED + application.checkSelfPermissionCompat(permissionName) == + PackageManager.PERMISSION_GRANTED } @Parcelize diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListViewModel.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListViewModel.kt index 176c68790..7e047a864 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListViewModel.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListViewModel.kt @@ -227,6 +227,13 @@ class FileListViewModel : ViewModel() { _isRequestingStorageAccessLiveData.value = value } + private val _isRequestingNotificationPermissionLiveData = MutableLiveData(false) + var isNotificationPermissionRequested: Boolean + get() = _isRequestingNotificationPermissionLiveData.valueCompat + set(value) { + _isRequestingNotificationPermissionLiveData.value = value + } + override fun onCleared() { _fileListLiveData.close() } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt new file mode 100644 index 000000000..062dae144 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.filelist + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import me.zhanghai.android.files.R +import me.zhanghai.android.files.util.show + +class ShowRequestNotificationPermissionInSettingsRationaleDialogFragment : AppCompatDialogFragment() { + private val listener: Listener + get() = requireParentFragment() as Listener + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setMessage(R.string.notification_permission_rationale_message) + .setPositiveButton(R.string.open_settings) { _, _ -> + listener.requestNotificationPermissionInSettings() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } + + companion object { + fun show(fragment: Fragment) { + ShowRequestNotificationPermissionInSettingsRationaleDialogFragment().show(fragment) + } + } + + interface Listener { + fun requestNotificationPermissionInSettings() + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt new file mode 100644 index 000000000..66cc5cee5 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.filelist + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import me.zhanghai.android.files.R +import me.zhanghai.android.files.util.show + +class ShowRequestNotificationPermissionRationaleDialogFragment : AppCompatDialogFragment() { + private val listener: Listener + get() = requireParentFragment() as Listener + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setMessage(R.string.notification_permission_rationale_message) + .setPositiveButton(android.R.string.ok) { _, _ -> + listener.requestNotificationPermission() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } + + companion object { + fun show(fragment: Fragment) { + ShowRequestNotificationPermissionRationaleDialogFragment().show(fragment) + } + } + + interface Listener { + fun requestNotificationPermission() + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt b/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt index 8fac77581..10af1587e 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt @@ -5,7 +5,6 @@ package me.zhanghai.android.files.util -import android.annotation.SuppressLint import android.app.Notification import android.app.Service import me.zhanghai.android.files.app.notificationManager @@ -15,8 +14,6 @@ class ForegroundNotificationManager(private val service: Service) { private var foregroundId = 0 - // TODO: Add POST_NOTIFICATIONS permission when targeting API 33. - @SuppressLint("MissingPermission") fun notify(id: Int, notification: Notification) { synchronized(notifications) { if (notifications.isEmpty()) { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 752e0ed93..4d68beaae 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -56,9 +56,11 @@ %1$,d 字节 + 应用需要管理所有文件的权限。请在接下来的系统设置中授予权限。 应用需要访问文件的权限。请在接下来的系统对话框中点击“允许”。 应用需要访问文件的权限。请在系统设置中授予“存储空间”权限。 - 应用需要管理所有文件的权限。请在接下来的系统设置中授予权限。 + 应用需要发布文件操作相关通知的权限。请在接下来的系统对话框中点击“允许”。 + 应用需要发布文件操作相关通知的权限。请在系统设置中授予“通知”权限。 后台期间动作 在应用处于后台期间采取动作 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1d0142815..612830b2f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -56,9 +56,11 @@ %1$,d 位元組 + 程式需要管理所有檔案的權限。請在接下來的系統設定中授予權限。 程式需要存取檔案的權限。請在接下來的系統對話框中點擊「允許」。 程式需要存取檔案的權限。請在系統設定中授予「儲存空間」權限。 - 程式需要管理所有檔案的權限。請在接下來的系統設定中授予權限。 + 程式需要發布檔案作業相關通知的權限。請在接下來的系統對話框中點擊「允許」。 + 程式需要發布檔案作業相關通知的權限。請在系統設定中授予「通知」權限。 背景期間動作 在應用程式處於背景期間採取動作 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 073b23f09..4c9632637 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,9 +59,13 @@ %1$,d bytes + App needs access to manage all files. Please allow the access in the upcoming system setting. + App needs permission to access files. Please click “ALLOW” in the upcoming system dialog. App needs permission to access files. Please grant the “Storage” permission in system settings. - App needs access to manage all files. Please allow the access in the upcoming system setting. + + App needs permission to post notifications about file operations. Please click “Allow” in the upcoming system dialog. + App needs permission to post notifications about file operations. Please grant the “Notification” permission in system settings. Actions while background Take actions while app is in the background From 9dbab7d90cb2b827f152c391c55808c5881f0a3a Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 10 Sep 2023 04:16:17 -0700 Subject: [PATCH 137/326] [Feature] Target Android 14. Add foreground service types and specify exported flag when register receivers. --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 8 ++++++-- .../android/files/storage/StorageVolumeListLiveData.kt | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 258a91c06..08ee53190 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,7 +34,7 @@ android { minSdk 21 // Not supporting notification runtime permission yet. //noinspection OldTargetApi - targetSdk 33 + targetSdk 34 versionCode 33 versionName '1.6.1' resValue 'string', 'app_version', versionName + ' (' + versionCode + ')' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 87e66a657..f7b28e0f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ + - + - + >() { init { loadValue() - application.registerReceiver( + application.registerReceiverCompat( object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { loadValue() @@ -36,7 +38,7 @@ object StorageVolumeListLiveData : LiveData>() { // The "file" data scheme is required to receive these broadcasts. // @see https://stackoverflow.com/a/7143298 addDataScheme(ContentResolver.SCHEME_FILE) - } + }, ContextCompat.RECEIVER_NOT_EXPORTED ) } From b603d4cae896b543a5c70c6db9bea7076d64fc58 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 11 Sep 2023 22:29:03 -0700 Subject: [PATCH 138/326] [Feature] Update libarchive-android. --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 08ee53190..a89d8ddfb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,7 +187,7 @@ dependencies { implementation 'me.zhanghai.android.appiconloader:appiconloader:1.5.0' implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.foregroundcompat:library:1.0.2' - implementation 'me.zhanghai.android.libarchive:library:1.0.0' + implementation 'me.zhanghai.android.libarchive:library:1.0.1' implementation 'me.zhanghai.android.libselinux:library:2.1.0' implementation 'me.zhanghai.android.retrofile:library:1.1.1' implementation 'me.zhanghai.android.systemuihelper:library:1.0.0' From 1ed731603f785208ac96cb9dd7d4c6462c3edd11 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 11 Sep 2023 22:30:06 -0700 Subject: [PATCH 139/326] [Refactor] Replace double quotes with single quotes in build.gradle. --- app/build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a89d8ddfb..e8d5fd7ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,7 +43,7 @@ android { resValue 'string', 'file_provider_authority', applicationId + '.file_provider' externalNativeBuild { cmake { - arguments "-DANDROID_STL=none" + arguments '-DANDROID_STL=none' } } } @@ -180,10 +180,10 @@ dependencies { } implementation 'org.bouncycastle:bcprov-jdk15to18:1.73' implementation platform('io.coil-kt:coil-bom:2.4.0') - implementation "io.coil-kt:coil" - implementation "io.coil-kt:coil-gif" - implementation "io.coil-kt:coil-svg" - implementation "io.coil-kt:coil-video" + implementation 'io.coil-kt:coil' + implementation 'io.coil-kt:coil-gif' + implementation 'io.coil-kt:coil-svg' + implementation 'io.coil-kt:coil-video' implementation 'me.zhanghai.android.appiconloader:appiconloader:1.5.0' implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.foregroundcompat:library:1.0.2' From edc71c8e88e121d474cb6b2dd22175fdde0d4ef9 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 14 Sep 2023 22:25:32 -0700 Subject: [PATCH 140/326] [Fix] Work around parsing AndroidManifest.xml on LGE devices. Bug: #1019 --- .../android/files/compat/LocaleConfigCompat.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/zhanghai/android/files/compat/LocaleConfigCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/LocaleConfigCompat.kt index bbeaa8f0f..ed98883f6 100644 --- a/app/src/main/java/me/zhanghai/android/files/compat/LocaleConfigCompat.kt +++ b/app/src/main/java/me/zhanghai/android/files/compat/LocaleConfigCompat.kt @@ -90,13 +90,25 @@ class LocaleConfigCompat(context: Context) { // @see com.android.server.pm.pkg.parsing.ParsingPackageUtils @XmlRes private fun getLocaleConfigResourceId(context: Context): Int { + // Java cookies starts at 1, while passing 0 (invalid cookie for Java) makes + // AssetManager pick the last asset containing such a file name. + // We should go over all the assets containing AndroidManifest.xml, however there's no + // API to do that, so the best we can do is to start from the first asset and iterate + // until we can't find the next asset containing AndroidManifest.xml. var cookie = 1 + var isAndroidManifestFound = false while (true) { val parser = try { context.assets.openXmlResourceParser(cookie, FILE_NAME_ANDROID_MANIFEST) } catch (e: FileNotFoundException) { - break + if (!isAndroidManifestFound) { + ++cookie + continue + } else { + break + } } + isAndroidManifestFound = true parser.use { do { if (parser.eventType != XmlPullParser.START_TAG) { From 08135e34ddf99dfd5ca89abfaece270fdcbb93bd Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 14 Sep 2023 23:09:23 -0700 Subject: [PATCH 141/326] [Feature] Support remote media thumbnail on Android 6.0+. --- .../files/coil/PathAttributesFetcher.kt | 3 +- .../MediaMetadataRetrieverPathExtensions.kt | 41 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt b/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt index 65f9c6a13..b32fd18bd 100644 --- a/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt +++ b/app/src/main/java/me/zhanghai/android/files/coil/PathAttributesFetcher.kt @@ -44,6 +44,7 @@ import me.zhanghai.android.files.settings.Settings import me.zhanghai.android.files.util.getDimensionPixelSize import me.zhanghai.android.files.util.getPackageArchiveInfoCompat import me.zhanghai.android.files.util.isGetPackageArchiveInfoCompatible +import me.zhanghai.android.files.util.isMediaMetadataRetrieverCompatible import me.zhanghai.android.files.util.runWithCancellationSignal import me.zhanghai.android.files.util.setDataSource import me.zhanghai.android.files.util.valueCompat @@ -119,7 +120,7 @@ class PathAttributesFetcher( if (mimeType != MimeType.GENERIC) mimeType.value else null, path.dataSource ) } - mimeType.isMedia && (path.isLinuxPath || path.isDocumentPath) -> { + mimeType.isMedia && path.isMediaMetadataRetrieverCompatible -> { val embeddedPicture = try { MediaMetadataRetriever().use { retriever -> retriever.setDataSource(path) diff --git a/app/src/main/java/me/zhanghai/android/files/util/MediaMetadataRetrieverPathExtensions.kt b/app/src/main/java/me/zhanghai/android/files/util/MediaMetadataRetrieverPathExtensions.kt index 23abe8132..ce0bb6273 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/MediaMetadataRetrieverPathExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/MediaMetadataRetrieverPathExtensions.kt @@ -5,14 +5,26 @@ package me.zhanghai.android.files.util +import android.media.MediaDataSource import android.media.MediaMetadataRetriever +import android.os.Build +import androidx.annotation.RequiresApi +import java8.nio.channels.SeekableByteChannel import java8.nio.file.Path +import me.zhanghai.android.files.provider.common.newByteChannel import me.zhanghai.android.files.provider.document.isDocumentPath import me.zhanghai.android.files.provider.document.resolver.DocumentResolver +import me.zhanghai.android.files.provider.ftp.isFtpPath import me.zhanghai.android.files.provider.linux.isLinuxPath +import java.io.IOException +import java.nio.ByteBuffer val Path.isMediaMetadataRetrieverCompatible: Boolean - get() = isLinuxPath || isDocumentPath + get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + !isFtpPath + } else { + isLinuxPath || isDocumentPath + } fun MediaMetadataRetriever.setDataSource(path: Path) { when { @@ -20,6 +32,33 @@ fun MediaMetadataRetriever.setDataSource(path: Path) { path.isDocumentPath -> DocumentResolver.openParcelFileDescriptor(path as DocumentResolver.Path, "r") .use { pfd -> setDataSource(pfd.fileDescriptor) } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { + val channel = try { + path.newByteChannel() + } catch (e: IOException) { + throw IllegalArgumentException(e) + } + setDataSource(PathMediaDataSource(channel)) + } else -> throw IllegalArgumentException(path.toString()) } } + +@RequiresApi(Build.VERSION_CODES.M) +private class PathMediaDataSource(private val channel: SeekableByteChannel) : MediaDataSource() { + @Throws(IOException::class) + override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { + channel.position(position) + return channel.read(ByteBuffer.wrap(buffer, offset, size)) + } + + @Throws(IOException::class) + override fun getSize(): Long { + return channel.size() + } + + @Throws(IOException::class) + override fun close() { + channel.close() + } +} From b0987f1ad97c7527c05909d006be1b19c51b35cf Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 15 Sep 2023 02:57:06 -0700 Subject: [PATCH 142/326] [Fix] Make supportsThumbnail support media for remote providers as well. Bug: #683 --- .../me/zhanghai/android/files/filelist/FileItemExtensions.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt index 374ee7382..1be1a109e 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt @@ -26,6 +26,7 @@ import me.zhanghai.android.files.provider.linux.isLinuxPath import me.zhanghai.android.files.settings.Settings import me.zhanghai.android.files.util.asFileName import me.zhanghai.android.files.util.isGetPackageArchiveInfoCompatible +import me.zhanghai.android.files.util.isMediaMetadataRetrieverCompatible import me.zhanghai.android.files.util.valueCompat import java.text.CollationKey @@ -70,7 +71,7 @@ val FileItem.supportsThumbnail: Boolean return when { mimeType.isApk && path.isGetPackageArchiveInfoCompatible -> true mimeType.isImage -> true - mimeType.isMedia && (path.isLinuxPath || path.isDocumentPath) -> true + mimeType.isMedia && path.isMediaMetadataRetrieverCompatible -> true mimeType.isPdf && (path.isLinuxPath || path.isDocumentPath) -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.P || Settings.SHOW_PDF_THUMBNAIL_PRE_28.valueCompat From 0d65ef1d5d11baba74dcf4fa0eeb4c781d942b30 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 15 Sep 2023 13:01:02 -0700 Subject: [PATCH 143/326] [Feature] Reduce touch target size for fast scroll thumb. So that it conflicts much less with the popup menu button. --- app/src/main/res/values/dimens.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index d15d4b6dd..34799c16a 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -47,4 +47,7 @@ 8dp 14dp 10dp + + + 24dp From 050a1aa87e45fdf901cf13b84b8a8f08818722d3 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Fri, 15 Sep 2023 20:25:39 -0700 Subject: [PATCH 144/326] [Fix] Update libarchive to fix 7z creation on older platforms. Fixes: #1027 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e8d5fd7ad..319278029 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -187,7 +187,7 @@ dependencies { implementation 'me.zhanghai.android.appiconloader:appiconloader:1.5.0' implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'me.zhanghai.android.foregroundcompat:library:1.0.2' - implementation 'me.zhanghai.android.libarchive:library:1.0.1' + implementation 'me.zhanghai.android.libarchive:library:1.0.2' implementation 'me.zhanghai.android.libselinux:library:2.1.0' implementation 'me.zhanghai.android.retrofile:library:1.1.1' implementation 'me.zhanghai.android.systemuihelper:library:1.0.0' From 5eeb8c650d91aefe825ca52aa6dd4b700b4e566a Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 16 Sep 2023 02:49:57 -0700 Subject: [PATCH 145/326] [Fix] Set onlyAlertOnce for file job & FTP server notifications. --- .../me/zhanghai/android/files/app/BackgroundActivityStarter.kt | 2 +- .../android/files/filejob/FileJobNotificationTemplate.kt | 3 ++- .../zhanghai/android/files/ftpserver/FtpServerNotification.kt | 3 ++- .../me/zhanghai/android/files/util/NotificationTemplate.kt | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt b/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt index 3f3392ca4..415d5aadf 100644 --- a/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt +++ b/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt @@ -18,7 +18,7 @@ import me.zhanghai.android.files.util.NotificationChannelTemplate import me.zhanghai.android.files.util.NotificationTemplate import me.zhanghai.android.files.util.startActivitySafe -val backgroundActivityStartNotificationTemplate: NotificationTemplate = +val backgroundActivityStartNotificationTemplate = NotificationTemplate( NotificationChannelTemplate( "background_activity_start", diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobNotificationTemplate.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobNotificationTemplate.kt index 897e40b91..165b0f992 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobNotificationTemplate.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobNotificationTemplate.kt @@ -11,7 +11,7 @@ import me.zhanghai.android.files.R import me.zhanghai.android.files.util.NotificationChannelTemplate import me.zhanghai.android.files.util.NotificationTemplate -val fileJobNotificationTemplate: NotificationTemplate = +val fileJobNotificationTemplate = NotificationTemplate( NotificationChannelTemplate( "file_job", @@ -23,6 +23,7 @@ val fileJobNotificationTemplate: NotificationTemplate = colorRes = R.color.color_primary, smallIcon = R.drawable.notification_icon, ongoing = true, + onlyAlertOnce = true, category = NotificationCompat.CATEGORY_PROGRESS, priority = NotificationCompat.PRIORITY_LOW ) diff --git a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerNotification.kt b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerNotification.kt index f1907c33b..b358ec580 100644 --- a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerNotification.kt +++ b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerNotification.kt @@ -18,7 +18,7 @@ import me.zhanghai.android.files.util.NotificationChannelTemplate import me.zhanghai.android.files.util.NotificationTemplate import me.zhanghai.android.files.util.createIntent -val ftpServerServiceNotificationTemplate: NotificationTemplate = +val ftpServerServiceNotificationTemplate = NotificationTemplate( NotificationChannelTemplate( "ftp_server", @@ -31,6 +31,7 @@ val ftpServerServiceNotificationTemplate: NotificationTemplate = smallIcon = R.drawable.notification_icon, contentTitleRes = R.string.ftp_server_notification_title, ongoing = true, + onlyAlertOnce = true, category = NotificationCompat.CATEGORY_SERVICE, priority = NotificationCompat.PRIORITY_LOW ) diff --git a/app/src/main/java/me/zhanghai/android/files/util/NotificationTemplate.kt b/app/src/main/java/me/zhanghai/android/files/util/NotificationTemplate.kt index 0d1071fca..c93a4132c 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/NotificationTemplate.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/NotificationTemplate.kt @@ -24,6 +24,7 @@ class NotificationTemplate( @StringRes val contentTitleRes: Int? = null, @StringRes val contentTextRes: Int? = null, val ongoing: Boolean? = null, + val onlyAlertOnce: Boolean? = null, val autoCancel: Boolean? = null, val category: String? = null, val priority: Int? = null @@ -35,6 +36,7 @@ class NotificationTemplate( contentTitleRes?.let { setContentTitle(context.getText(contentTitleRes)) } contentTextRes?.let { setContentText(context.getText(contentTextRes)) } ongoing?.let { setOngoing(it) } + onlyAlertOnce?.let { setOnlyAlertOnce(it) } autoCancel?.let { setAutoCancel(it) } category?.let { setCategory(it) } this@NotificationTemplate.priority?.let { priority = it } From 892229c6634a635c19198534fe1d85a3a9429ced Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 16 Sep 2023 03:47:23 -0700 Subject: [PATCH 146/326] [Fix] Don't launch open intent if copy failed. --- .../main/java/me/zhanghai/android/files/filejob/FileJobs.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt index 7fb6e2344..8c0e2d0fe 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt @@ -1472,7 +1472,10 @@ private fun FileJob.open( val targetFile = cacheDirectory.resolveForeign(targetFileName) val transferInfo = TransferInfo(scanInfo, cacheDirectory) val actionAllInfo = ActionAllInfo(replace = true) - copy(file, targetFile, isExtract, transferInfo, actionAllInfo) + val copied = copy(file, targetFile, isExtract, transferInfo, actionAllInfo) + if (!copied) { + return + } BackgroundActivityStarter.startActivity( intentCreator(targetFile), getString(notificationTitleFormatRes, targetFileName), getString(notificationTextRes), service From 1c156ff625be67c9c9d1d13debec0473491ceeca Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 18 Sep 2023 00:33:41 -0700 Subject: [PATCH 147/326] [Feature] Add badge for encrypted files. --- .../android/files/filelist/FileListAdapter.kt | 3 +++ .../provider/archive/ArchiveFileAttributes.kt | 9 +++++++-- .../provider/archive/archiver/ArchiveReader.kt | 2 +- .../provider/archive/archiver/ReadArchive.kt | 6 ++++-- .../provider/common/EncryptedFileAttributes.kt | 15 +++++++++++++++ .../res/drawable/encrypted_badge_icon_18dp.xml | 18 ++++++++++++++++++ 6 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/me/zhanghai/android/files/provider/common/EncryptedFileAttributes.kt create mode 100644 app/src/main/res/drawable/encrypted_badge_icon_18dp.xml diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt index 874b3cf77..8ab0360b6 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListAdapter.kt @@ -32,6 +32,7 @@ import me.zhanghai.android.files.file.formatShort import me.zhanghai.android.files.file.iconRes import me.zhanghai.android.files.file.isApk import me.zhanghai.android.files.provider.archive.isArchivePath +import me.zhanghai.android.files.provider.common.isEncrypted import me.zhanghai.android.files.settings.Settings import me.zhanghai.android.files.ui.AnimatedListAdapter import me.zhanghai.android.files.ui.CheckableForegroundLinearLayout @@ -301,6 +302,8 @@ class FileListAdapter( } else { R.drawable.symbolic_link_badge_icon_18dp } + } else if (file.attributesNoFollowLinks.isEncrypted()) { + R.drawable.encrypted_badge_icon_18dp } else { null } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt index dfb161433..4050b8767 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributes.kt @@ -13,6 +13,7 @@ import kotlinx.parcelize.WriteWith import me.zhanghai.android.files.provider.archive.archiver.ReadArchive import me.zhanghai.android.files.provider.common.AbstractPosixFileAttributes import me.zhanghai.android.files.provider.common.ByteString +import me.zhanghai.android.files.provider.common.EncryptedFileAttributes import me.zhanghai.android.files.provider.common.FileTimeParceler import me.zhanghai.android.files.provider.common.PosixFileModeBit import me.zhanghai.android.files.provider.common.PosixFileType @@ -31,8 +32,11 @@ internal class ArchiveFileAttributes( override val group: PosixGroup?, override val mode: Set?, override val seLinuxContext: ByteString?, + private val isEncrypted: Boolean, private val entryName: String -) : AbstractPosixFileAttributes() { +) : AbstractPosixFileAttributes(), EncryptedFileAttributes { + override fun isEncrypted(): Boolean = isEncrypted + fun entryName(): String = entryName companion object { @@ -47,10 +51,11 @@ internal class ArchiveFileAttributes( val group = entry.group val mode = entry.mode val seLinuxContext = null + val isEncrypted = entry.isEncrypted val entryName = entry.name return ArchiveFileAttributes( lastModifiedTime, lastAccessTime, creationTime, type, size, fileKey, owner, group, - mode, seLinuxContext, entryName + mode, seLinuxContext, isEncrypted, entryName ) } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt index 352f20610..66bc4cdb7 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt @@ -85,7 +85,7 @@ object ArchiveReader { private fun createDirectoryEntry(name: String): ReadArchive.Entry { require(!name.endsWith("/")) { "name $name should not end with a slash" } return ReadArchive.Entry( - name, null, null, null, PosixFileType.DIRECTORY, 0, null, null, + name, false, null, null, null, PosixFileType.DIRECTORY, 0, null, null, PosixFileMode.DIRECTORY_DEFAULT, null ) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt index a6aa0d924..59051fece 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt @@ -136,6 +136,7 @@ class ReadArchive : Closeable { ?: throw ArchiveException( Archive.ERRNO_FATAL, "pathname == null && pathnameUtf8 == null" ) + val isEncrypted = ArchiveEntry.isEncrypted(entry) val stat = ArchiveEntry.stat(entry) val lastModifiedTime = if (ArchiveEntry.mtimeIsSet(entry)) { FileTime.from( @@ -177,8 +178,8 @@ class ReadArchive : Closeable { val symbolicLinkTarget = getEntryString(ArchiveEntry.symlinkUtf8(entry), ArchiveEntry.symlink(entry), charset) return Entry( - name, lastModifiedTime, lastAccessTime, creationTime, type, size, owner, group, mode, - symbolicLinkTarget + name, isEncrypted, lastModifiedTime, lastAccessTime, creationTime, type, size, owner, + group, mode, symbolicLinkTarget ) } @@ -209,6 +210,7 @@ class ReadArchive : Closeable { class Entry( val name: String, + val isEncrypted: Boolean, val lastModifiedTime: FileTime?, val lastAccessTime: FileTime?, val creationTime: FileTime?, diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/EncryptedFileAttributes.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/EncryptedFileAttributes.kt new file mode 100644 index 000000000..1b939c312 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/EncryptedFileAttributes.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.provider.common + +import java8.nio.file.attribute.BasicFileAttributes + +interface EncryptedFileAttributes { + fun isEncrypted(): Boolean +} + +fun BasicFileAttributes.isEncrypted(): Boolean = + if (this is EncryptedFileAttributes) isEncrypted() else false diff --git a/app/src/main/res/drawable/encrypted_badge_icon_18dp.xml b/app/src/main/res/drawable/encrypted_badge_icon_18dp.xml new file mode 100644 index 000000000..c75dce6b3 --- /dev/null +++ b/app/src/main/res/drawable/encrypted_badge_icon_18dp.xml @@ -0,0 +1,18 @@ + + + + + + + + From 3433a7442fbb23e689a74f0b48b6103d09905e9b Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 18 Sep 2023 21:41:58 -0700 Subject: [PATCH 148/326] [Refactor] Minor refactoring. --- .../main/java/me/zhanghai/android/files/filejob/FileJobs.kt | 4 ++-- .../android/files/provider/archive/archiver/ArchiveReader.kt | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt index 8c0e2d0fe..91be7cc79 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt @@ -478,7 +478,7 @@ private fun FileJob.showActionDialog( neutralButtonText: CharSequence? ): ActionResult = try { - runBlocking { + runBlocking { suspendCoroutine { continuation -> BackgroundActivityStarter.startActivity( FileJobActionDialogActivity::class.createIntent().putArgs( @@ -521,7 +521,7 @@ private fun FileJob.showConflictDialog( type: CopyMoveType ): ConflictResult = try { - runBlocking { + runBlocking { suspendCoroutine { continuation -> BackgroundActivityStarter.startActivity( FileJobConflictDialogActivity::class.createIntent().putArgs( diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt index 66bc4cdb7..841e0fc1f 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt @@ -112,9 +112,8 @@ object ArchiveReader { val (archive, closeable) = openArchive(file) var successful = false return try { - var currentEntry: ReadArchive.Entry? = null while (true) { - currentEntry = archive.readEntry(charset) ?: break + val currentEntry = archive.readEntry(charset) ?: break if (currentEntry.name != entry.name) { continue } From 38fa9e1d9afa0ac7fdd3d46b90767d699028af37 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 18 Sep 2023 23:04:10 -0700 Subject: [PATCH 149/326] [Feature] Properly propagate exception in archive and throw ArchivePasswordRequiredException. --- .../archive/ArchiveExceptionExtensions.kt | 94 +++++++++++++++++++ .../ArchivePasswordRequiredException.kt | 17 ++++ .../archive/LocalArchiveFileSystem.kt | 34 +++++-- .../archive/archiver/ArchiveReader.kt | 19 +--- .../provider/archive/archiver/ReadArchive.kt | 29 +++--- .../common/UserActionRequiredException.kt | 17 ++++ 6 files changed, 172 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/provider/common/UserActionRequiredException.kt diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt new file mode 100644 index 000000000..590f6ef33 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.provider.archive + +import android.system.OsConstants +import java8.nio.file.FileSystemException +import java8.nio.file.Path +import me.zhanghai.android.files.provider.common.DelegateInputStream +import me.zhanghai.android.libarchive.ArchiveException +import java.io.IOException +import java.io.InputStream +import java.io.InterruptedIOException + +// See also libarchive/archive_platform.h . +private const val ARCHIVE_ERRNO_MISC = -1 + +fun ArchiveException.toFileSystemOrInterruptedIOException( + file: String?, + other: String? = null +): IOException = + when { + // See also ReadArchive.toArchiveException . + code == OsConstants.EINTR -> InterruptedIOException(message) + // See also libarchive/archive_read_support_format_zip.c . + code == ARCHIVE_ERRNO_MISC && ( + message == "Incorrect passphrase" || message == "Passphrase required for this entry" + ) -> ArchivePasswordRequiredException(file, other, message) + else -> FileSystemException(file, other, message) + }.apply { initCause(this@toFileSystemOrInterruptedIOException) } + +class ArchiveExceptionInputStream( + inputStream: InputStream, + private val file: Path +) : DelegateInputStream(inputStream) { + @Throws(IOException::class) + override fun read(): Int = + try { + super.read() + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } + + @Throws(IOException::class) + override fun read(b: ByteArray): Int = + try { + super.read(b) + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } + + @Throws(IOException::class) + override fun read(b: ByteArray, off: Int, len: Int): Int = + try { + super.read(b, off, len) + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } + + @Throws(IOException::class) + override fun skip(n: Long): Long = try { + super.skip(n) + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } + + @Throws(IOException::class) + override fun available(): Int = + try { + super.available() + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } + + @Throws(IOException::class) + override fun close() { + try { + super.close() + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } + } + + @Throws(IOException::class) + override fun reset() { + try { + super.reset() + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt new file mode 100644 index 000000000..1572b9a48 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.provider.archive + +import android.content.Intent +import me.zhanghai.android.files.provider.common.UserActionRequiredException + +class ArchivePasswordRequiredException : UserActionRequiredException { + constructor(file: String?) : super(file) + + constructor(file: String?, other: String?, reason: String?) : super(file, other, reason) + + override fun getUserAction(): Intent = TODO() +} diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt index 10180388a..d099e0527 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt @@ -10,6 +10,7 @@ import java8.nio.file.FileStore import java8.nio.file.FileSystem import java8.nio.file.NoSuchFileException import java8.nio.file.NotDirectoryException +import java8.nio.file.NotLinkException import java8.nio.file.Path import java8.nio.file.PathMatcher import java8.nio.file.WatchService @@ -20,7 +21,9 @@ import me.zhanghai.android.files.provider.archive.archiver.ReadArchive import me.zhanghai.android.files.provider.common.ByteString import me.zhanghai.android.files.provider.common.ByteStringBuilder import me.zhanghai.android.files.provider.common.ByteStringListPathCreator +import me.zhanghai.android.files.provider.common.IsDirectoryException import me.zhanghai.android.files.provider.common.toByteString +import me.zhanghai.android.libarchive.ArchiveException import java.io.IOException import java.io.InputStream @@ -56,7 +59,7 @@ internal class LocalArchiveFileSystem( @Throws(IOException::class) fun getEntry(path: Path): ReadArchive.Entry = synchronized(lock) { - ensureEntriesLocked() + ensureEntriesLocked(path) getEntryLocked(path) } @@ -69,15 +72,23 @@ internal class LocalArchiveFileSystem( @Throws(IOException::class) fun newInputStream(file: Path): InputStream = synchronized(lock) { - ensureEntriesLocked() + ensureEntriesLocked(file) val entry = getEntryLocked(file) - ArchiveReader.newInputStream(archiveFile, entry) + if (entry.isDirectory) { + throw IsDirectoryException(file.toString()) + } + val inputStream = try { + ArchiveReader.newInputStream(archiveFile, entry) + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } ?: throw NoSuchFileException(file.toString()) + ArchiveExceptionInputStream(inputStream, file) } @Throws(IOException::class) fun getDirectoryChildren(directory: Path): List = synchronized(lock) { - ensureEntriesLocked() + ensureEntriesLocked(directory) val entry = getEntryLocked(directory) if (!entry.isDirectory) { throw NotDirectoryException(directory.toString()) @@ -88,9 +99,12 @@ internal class LocalArchiveFileSystem( @Throws(IOException::class) fun readSymbolicLink(link: Path): String = synchronized(lock) { - ensureEntriesLocked() + ensureEntriesLocked(link) val entry = getEntryLocked(link) - ArchiveReader.readSymbolicLink(archiveFile, entry) + if (!entry.isSymbolicLink) { + throw NotLinkException(link.toString()) + } + entry.symbolicLinkTarget ?: "" } fun refresh() { @@ -103,12 +117,16 @@ internal class LocalArchiveFileSystem( } @Throws(IOException::class) - private fun ensureEntriesLocked() { + private fun ensureEntriesLocked(file: Path) { if (!isOpen) { throw ClosedFileSystemException() } if (isRefreshNeeded) { - val entriesAndTree = ArchiveReader.readEntries(archiveFile, rootDirectory) + val entriesAndTree = try { + ArchiveReader.readEntries(archiveFile, rootDirectory) + } catch (e: ArchiveException) { + throw e.toFileSystemOrInterruptedIOException(file.toString()) + } entries = entriesAndTree.first tree = entriesAndTree.second isRefreshNeeded = false diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt index 841e0fc1f..329568204 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt @@ -8,15 +8,12 @@ package me.zhanghai.android.files.provider.archive.archiver import androidx.preference.PreferenceManager import java8.nio.channels.SeekableByteChannel import java8.nio.charset.StandardCharsets -import java8.nio.file.NoSuchFileException -import java8.nio.file.NotLinkException import java8.nio.file.Path import me.zhanghai.android.files.R import me.zhanghai.android.files.provider.common.DelegateForceableSeekableByteChannel import me.zhanghai.android.files.provider.common.DelegateInputStream import me.zhanghai.android.files.provider.common.DelegateNonForceableSeekableByteChannel import me.zhanghai.android.files.provider.common.ForceableChannel -import me.zhanghai.android.files.provider.common.IsDirectoryException import me.zhanghai.android.files.provider.common.PosixFileMode import me.zhanghai.android.files.provider.common.PosixFileType import me.zhanghai.android.files.provider.common.newByteChannel @@ -104,10 +101,7 @@ object ArchiveReader { } @Throws(IOException::class) - fun newInputStream(file: Path, entry: ReadArchive.Entry): InputStream { - if (entry.isDirectory) { - throw IsDirectoryException(file.toString()) - } + fun newInputStream(file: Path, entry: ReadArchive.Entry): InputStream? { val charset = archiveFileNameCharset val (archive, closeable) = openArchive(file) var successful = false @@ -123,7 +117,7 @@ object ArchiveReader { if (successful) { CloseableInputStream(archive.newDataInputStream(), closeable) } else { - throw NoSuchFileException(file.toString()) + null } } finally { if (!successful) { @@ -132,6 +126,7 @@ object ArchiveReader { } } + @Throws(IOException::class) private fun openArchive(file: Path): Pair { val channel = try { CacheSizeSeekableByteChannel(file.newByteChannel()) @@ -232,12 +227,4 @@ object ArchiveReader { closeable.close() } } - - @Throws(IOException::class) - fun readSymbolicLink(file: Path, entry: ReadArchive.Entry): String { - if (entry.type != PosixFileType.SYMBOLIC_LINK) { - throw NotLinkException(file.toString()) - } - return entry.symbolicLinkTarget ?: "" - } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt index 59051fece..dda766857 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt @@ -22,6 +22,7 @@ import org.threeten.bp.Instant import java.io.Closeable import java.io.IOException import java.io.InputStream +import java.io.InterruptedIOException import java.nio.ByteBuffer import java.nio.charset.Charset @@ -42,7 +43,7 @@ class ReadArchive : Closeable { val bytesRead = try { inputStream.read(buffer.array()) } catch (e: IOException) { - throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.read", e) + throw e.toArchiveException("InputStream.read") } if (bytesRead != -1) { buffer.limit(bytesRead) @@ -55,7 +56,7 @@ class ReadArchive : Closeable { try { inputStream.skip(request) } catch (e: IOException) { - throw ArchiveException(Archive.ERRNO_FATAL, "InputStream.skip", e) + throw e.toArchiveException("InputStream.skip") } } Archive.readOpen1(archive) @@ -81,7 +82,7 @@ class ReadArchive : Closeable { val bytesRead = try { channel.read(buffer) } catch (e: IOException) { - throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.read", e) + throw e.toArchiveException("SeekableByteChannel.read") } if (bytesRead != -1) { buffer.flip() @@ -94,7 +95,7 @@ class ReadArchive : Closeable { try { channel.position(channel.position() + request) } catch (e: IOException) { - throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e) + throw e.toArchiveException("SeekableByteChannel.position") } request } @@ -112,7 +113,7 @@ class ReadArchive : Closeable { } channel.position(newPosition) } catch (e: IOException) { - throw ArchiveException(Archive.ERRNO_FATAL, "SeekableByteChannel.position", e) + throw e.toArchiveException("SeekableByteChannel.position") } newPosition } @@ -125,6 +126,12 @@ class ReadArchive : Closeable { } } + private fun IOException.toArchiveException(message: String): ArchiveException = + when (this) { + is InterruptedIOException -> ArchiveException(OsConstants.EINTR, message, this) + else -> ArchiveException(Archive.ERRNO_FATAL, message, this) + } + @Throws(ArchiveException::class) fun readEntry(charset: Charset): Entry? { val entry = Archive.readNextHeader(archive) @@ -186,15 +193,6 @@ class ReadArchive : Closeable { private fun getEntryString(stringUtf8: String?, string: ByteArray?, charset: Charset): String? = stringUtf8 ?: string?.toString(charset) - fun hasEncryptedEntries(): Boolean? { - val hasEncryptedEntries = Archive.readHasEncryptedEntries(archive) - return when { - hasEncryptedEntries > 0 -> true - hasEncryptedEntries == 0 -> false - else -> null - } - } - @Throws(ArchiveException::class) fun newDataInputStream(): InputStream = DataInputStream() @@ -223,6 +221,9 @@ class ReadArchive : Closeable { ) { val isDirectory: Boolean get() = type == PosixFileType.DIRECTORY + + val isSymbolicLink: Boolean + get() = type == PosixFileType.SYMBOLIC_LINK } private inner class DataInputStream : InputStream() { diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/UserActionRequiredException.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/UserActionRequiredException.kt new file mode 100644 index 000000000..c95022446 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/UserActionRequiredException.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.provider.common + +import android.content.Intent +import java8.nio.file.FileSystemException + +abstract class UserActionRequiredException : FileSystemException { + constructor(file: String?) : super(file) + + constructor(file: String?, other: String?, reason: String?) : super(file, other, reason) + + abstract fun getUserAction(): Intent +} From f7d5056c29c718468c153c9b33be6ae93e325102 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Wed, 20 Sep 2023 21:31:23 -0700 Subject: [PATCH 150/326] [Fix] Acquire wifi and wake lock for file jobs. Fixes: #1031 --- .../android/files/filejob/FileJobService.kt | 32 ++++++++++++----- .../files/ftpserver/FtpServerService.kt | 11 +++--- .../WakeWifiLock.kt} | 35 ++++++++++--------- 3 files changed, 48 insertions(+), 30 deletions(-) rename app/src/main/java/me/zhanghai/android/files/{ftpserver/FtpServerWakeLock.kt => util/WakeWifiLock.kt} (52%) diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt index ef34013e1..a6550c5f3 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt @@ -16,11 +16,14 @@ import me.zhanghai.android.files.provider.common.PosixFileModeBit import me.zhanghai.android.files.provider.common.PosixGroup import me.zhanghai.android.files.provider.common.PosixUser import me.zhanghai.android.files.util.ForegroundNotificationManager +import me.zhanghai.android.files.util.WakeWifiLock import me.zhanghai.android.files.util.removeFirst import java.util.concurrent.Executors import java.util.concurrent.Future class FileJobService : Service() { + private lateinit var wakeWifiLock: WakeWifiLock + internal lateinit var notificationManager: ForegroundNotificationManager private set @@ -31,6 +34,7 @@ class FileJobService : Service() { override fun onCreate() { super.onCreate() + wakeWifiLock = WakeWifiLock(FileJobService::class.java.simpleName) notificationManager = ForegroundNotificationManager(this) instance = this @@ -39,27 +43,32 @@ class FileJobService : Service() { } } + override fun onBind(intent: Intent): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY + + private val jobCount: Int + get() = synchronized(runningJobs) { runningJobs.size } + private fun startJob(job: FileJob) { // Synchronize on runningJobs to prevent a job from removing itself before being added. synchronized(runningJobs) { val future = executorService.submit { job.runOn(this) - synchronized(runningJobs) { runningJobs.remove(job) } + synchronized(runningJobs) { + runningJobs.remove(job) + updateWakeWifiLockLocked() + } } runningJobs[job] = future + updateWakeWifiLockLocked() } } - override fun onBind(intent: Intent): IBinder? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY - - private val jobCount: Int - get() = synchronized(runningJobs) { runningJobs.size } - private fun cancelJob(id: Int) { synchronized(runningJobs) { runningJobs.removeFirst { it.key.id == id }?.value?.cancel(true) + updateWakeWifiLockLocked() } } @@ -72,9 +81,16 @@ class FileJobService : Service() { while (runningJobs.isNotEmpty()) { runningJobs.removeFirst().value.cancel(true) } + updateWakeWifiLockLocked() } } + // Synchronize on runningJobs to avoid the potential race condition that the lock is + // acquired after all jobs are finished in a very short time. + private fun updateWakeWifiLockLocked() { + wakeWifiLock.isAcquired = jobCount > 0 + } + companion object { private var instance: FileJobService? = null diff --git a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerService.kt b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerService.kt index 5f7171c19..1e0c9aedd 100644 --- a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerService.kt +++ b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerService.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import me.zhanghai.android.files.compat.mainExecutorCompat import me.zhanghai.android.files.settings.Settings +import me.zhanghai.android.files.util.WakeWifiLock import me.zhanghai.android.files.util.showToast import me.zhanghai.android.files.util.valueCompat import java.util.concurrent.Executors @@ -26,7 +27,7 @@ class FtpServerService : Service() { _stateLiveData.value = value } - private lateinit var wakeLock: FtpServerWakeLock + private lateinit var wakeWifiLock: WakeWifiLock private lateinit var notification: FtpServerNotification @@ -37,7 +38,7 @@ class FtpServerService : Service() { override fun onCreate() { super.onCreate() - wakeLock = FtpServerWakeLock() + wakeWifiLock = WakeWifiLock(FtpServerService::class.java.simpleName) notification = FtpServerNotification(this) executeStart() } @@ -57,7 +58,7 @@ class FtpServerService : Service() { if (state == State.STARTING || state == State.RUNNING) { return } - wakeLock.acquire() + wakeWifiLock.isAcquired = true notification.startForeground() state = State.STARTING executorService.execute { doStart() } @@ -67,7 +68,7 @@ class FtpServerService : Service() { state = State.STOPPED showToast(exception.toString()) notification.stopForeground() - wakeLock.release() + wakeWifiLock.isAcquired = false stopSelf() } @@ -78,7 +79,7 @@ class FtpServerService : Service() { state = State.STOPPING executorService.execute { doStop() } notification.stopForeground() - wakeLock.release() + wakeWifiLock.isAcquired = false } @WorkerThread diff --git a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerWakeLock.kt b/app/src/main/java/me/zhanghai/android/files/util/WakeWifiLock.kt similarity index 52% rename from app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerWakeLock.kt rename to app/src/main/java/me/zhanghai/android/files/util/WakeWifiLock.kt index f4a260fda..9cfd15ca5 100644 --- a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerWakeLock.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/WakeWifiLock.kt @@ -3,31 +3,32 @@ * All Rights Reserved. */ -package me.zhanghai.android.files.ftpserver +package me.zhanghai.android.files.util import android.net.wifi.WifiManager import android.os.PowerManager import me.zhanghai.android.files.app.powerManager import me.zhanghai.android.files.app.wifiManager -class FtpServerWakeLock { - private val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOCK_TAG) +class WakeWifiLock(tag: String) { + private val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag) .apply { setReferenceCounted(false) } private val wifiLock = - wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, LOCK_TAG) + wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, tag) .apply { setReferenceCounted(false) } - fun acquire() { - wakeLock.acquire() - wifiLock.acquire() - } - - fun release() { - wifiLock.release() - wakeLock.release() - } - - companion object { - private val LOCK_TAG = FtpServerWakeLock::class.java.simpleName - } + var isAcquired: Boolean = false + set(value) { + if (field == value) { + return + } + if (value) { + wakeLock.acquire() + wifiLock.acquire() + } else { + wifiLock.release() + wakeLock.release() + } + field = value + } } From c31e09585d04aff5db50bd4148f991cf8d379801 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 23 Sep 2023 16:40:48 -0700 Subject: [PATCH 151/326] [Fix] Set offscreen page limit to 1 for ViewPager2 in image viewer. Because it was 1 for the old ViewPager and provided better user experience. Fixes: #1033 --- .../files/viewer/image/ImageViewerFragment.kt | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt b/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt index eda4b9e4e..159fccd91 100644 --- a/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt @@ -100,17 +100,22 @@ class ImageViewerFragment : Fragment(), ConfirmDeleteDialogFragment.Listener { } // This will set up window flags. systemUiHelper.show() - adapter = ImageViewerAdapter(viewLifecycleOwner) { systemUiHelper.toggle() } - adapter.replace(paths) - binding.viewPager.adapter = adapter - // ViewPager saves its position and will restore it later. - binding.viewPager.setCurrentItem(args.position, false) - binding.viewPager.setPageTransformer(DepthPageTransformer) - binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - updateTitle() - } - }) + adapter = ImageViewerAdapter(viewLifecycleOwner) { systemUiHelper.toggle() }.apply { + replace(paths) + } + binding.viewPager.apply { + // 1 is the default for the old androidx.viewpager.widget.ViewPager. + offscreenPageLimit = 1 + adapter = this@ImageViewerFragment.adapter + // ViewPager saves its position and will restore it later. + setCurrentItem(args.position, false) + setPageTransformer(DepthPageTransformer) + registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + updateTitle() + } + }) + } } override fun onViewStateRestored(savedInstanceState: Bundle?) { From 0b432da8323f20137b3031ad4836eb3c392b19ef Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 25 Sep 2023 03:53:24 -0700 Subject: [PATCH 152/326] [Fix] Fix blank screen after deletion when we have offscreenPageLimit = 1. --- .../android/files/viewer/image/ImageViewerFragment.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt b/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt index 159fccd91..cb082f449 100644 --- a/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/viewer/image/ImageViewerFragment.kt @@ -15,6 +15,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.doOnPreDraw import androidx.fragment.app.Fragment import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.viewpager2.widget.ViewPager2 @@ -179,6 +180,9 @@ class ImageViewerFragment : Fragment(), ConfirmDeleteDialogFragment.Listener { binding.viewPager.currentItem = paths.lastIndex } updateTitle() + // Work around blank screen due to ViewPager2.PageTransformer not being called (and thus the + // next item keeps its 0 alpha) when we have offscreenPageLimit = 1. + binding.viewPager.doOnPreDraw { binding.viewPager.requestTransform() } } private fun updateTitle() { From 0d7deffeaa8f25c933f8a744e64e5e00f9318b89 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 25 Sep 2023 22:16:55 -0700 Subject: [PATCH 153/326] [Fix] Sync file item binding logic from file list adapter to file job conflict dialog. --- .../filejob/FileJobConflictDialogFragment.kt | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt index b41c121e8..d55bb8721 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt @@ -38,6 +38,7 @@ import me.zhanghai.android.files.file.iconRes import me.zhanghai.android.files.file.lastModifiedInstant import me.zhanghai.android.files.filelist.appDirectoryPackageName import me.zhanghai.android.files.filelist.supportsThumbnail +import me.zhanghai.android.files.provider.common.isEncrypted import me.zhanghai.android.files.util.ParcelableArgs import me.zhanghai.android.files.util.ParcelableState import me.zhanghai.android.files.util.RemoteCallback @@ -151,37 +152,51 @@ class FileJobConflictDialogFragment : AppCompatDialogFragment() { descriptionText: TextView ) { val path = file.path - iconImage.setImageResource(file.mimeType.iconRes) - iconImage.isVisible = true - thumbnailImage.dispose() - thumbnailImage.setImageDrawable(null) + iconImage.apply { + isVisible = true + setImageResource(file.mimeType.iconRes) + } val attributes = file.attributes - if (file.supportsThumbnail) { - thumbnailImage.load(path to attributes) { - listener { _, _ -> iconImage.isVisible = false } + thumbnailImage.apply { + dispose() + setImageDrawable(null) + val supportsThumbnail = file.supportsThumbnail + isVisible = supportsThumbnail + if (supportsThumbnail) { + load(path to attributes) { + listener { _, _ -> iconImage.isVisible = false } + } } } - appIconBadgeImage.dispose() - appIconBadgeImage.setImageDrawable(null) - val appDirectoryPackageName = file.appDirectoryPackageName - val hasAppIconBadge = appDirectoryPackageName != null - appIconBadgeImage.isVisible = hasAppIconBadge - if (hasAppIconBadge) { - appIconBadgeImage.load(AppIconPackageName(appDirectoryPackageName!!)) + appIconBadgeImage.apply { + dispose() + setImageDrawable(null) + val appDirectoryPackageName = file.appDirectoryPackageName + val hasAppIconBadge = appDirectoryPackageName != null + isVisible = hasAppIconBadge + if (hasAppIconBadge) { + load(AppIconPackageName(appDirectoryPackageName!!)) + } } - val badgeIconRes = if (file.attributesNoFollowLinks.isSymbolicLink) { - if (file.isSymbolicLinkBroken) { - R.drawable.error_badge_icon_18dp + badgeImage.apply { + val badgeIconRes = if (file.attributesNoFollowLinks.isSymbolicLink) { + if (file.isSymbolicLinkBroken) { + R.drawable.error_badge_icon_18dp + } else { + R.drawable.symbolic_link_badge_icon_18dp + } + } else if (file.attributesNoFollowLinks.isEncrypted()) { + R.drawable.encrypted_badge_icon_18dp } else { - R.drawable.symbolic_link_badge_icon_18dp + null + } + val hasBadge = badgeIconRes != null + isVisible = hasBadge + if (hasBadge) { + setImageResource(badgeIconRes!!) + } else { + setImageDrawable(null) } - } else { - null - } - val hasBadge = badgeIconRes != null - badgeImage.isVisible = hasBadge - if (hasBadge) { - badgeImage.setImageResource(badgeIconRes!!) } val lastModificationTime = attributes.lastModifiedInstant .formatShort(descriptionText.context) From d3b9b909a886c704cd12d9a362455b72ad3026a4 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Mon, 25 Sep 2023 22:54:21 -0700 Subject: [PATCH 154/326] [Fix] Make file job conflict dialog resize with IME. And clear alt focusable im earlier to be conceptually better. --- .../android/files/filejob/FileJobConflictDialogFragment.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt index d55bb8721..5cebbe815 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt @@ -139,7 +139,10 @@ class FileJobConflictDialogFragment : AppCompatDialogFragment() { .setNegativeButton(R.string.skip, ::onDialogButtonClick) .setNeutralButton(android.R.string.cancel, ::onDialogButtonClick) .create() - .apply { setCanceledOnTouchOutside(false) } + .apply { + setCanceledOnTouchOutside(false) + window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + } } /** @see me.zhanghai.android.files.filelist.FileListAdapter.onBindViewHolder */ @@ -250,10 +253,10 @@ class FileJobConflictDialogFragment : AppCompatDialogFragment() { if (binding.root.parent == null) { val dialog = requireDialog() as AlertDialog + dialog.window!!.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) val scrollView = dialog.requireViewByIdCompat(R.id.scrollView) val linearLayout = scrollView.getChildAt(0) as LinearLayout linearLayout.addView(binding.root) - dialog.window!!.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) } } From c78e571b69f2c56ccec7c1c6eb4da21661efe9ce Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 28 Sep 2023 18:01:18 -0700 Subject: [PATCH 155/326] [Feature] Update AGP to 8.1.2. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 02730f286..a11b72754 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' + classpath 'com.android.tools.build:gradle:8.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong From d0ddf5cfeea400b45eb1cc15ffec14a30a2d07ee Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Thu, 28 Sep 2023 18:21:35 -0700 Subject: [PATCH 156/326] Revert "[Fix] Work around R8 bug." This reverts commit 1ae5a81d3203b624b8d9f5eadf2b567deedfbefa. Fixes: #991 --- gradle.properties | 3 --- 1 file changed, 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index d3222ccd1..3ef4f5785 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,9 +16,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -# See https://issuetracker.google.com/issues/296654327 . -android.enableR8.fullMode=false - # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn From d30e85f4aa838f8ee741a23758c8c3276b7accc5 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 30 Sep 2023 17:50:45 -0700 Subject: [PATCH 157/326] [Feature] Support reading encrypted ZIP archives. Fixes: #1017 --- app/src/main/AndroidManifest.xml | 7 +- .../provider/remote/IRemoteFileService.aidl | 2 + .../ArchivePasswordDialogActivity.kt | 44 ++++ .../ArchivePasswordDialogFragment.kt | 176 ++++++++++++++ .../filejob/FileJobConflictDialogActivity.kt | 14 -- .../filejob/FileJobConflictDialogFragment.kt | 4 +- ...FileJobAction.kt => FileJobErrorAction.kt} | 2 +- ...ivity.kt => FileJobErrorDialogActivity.kt} | 14 +- ...gment.kt => FileJobErrorDialogFragment.kt} | 36 +-- ...nViewModel.kt => FileJobErrorViewModel.kt} | 2 +- .../android/files/filejob/FileJobs.kt | 218 +++++++++++++----- .../archive/ArchiveExceptionExtensions.kt | 23 +- .../provider/archive/ArchiveFileSystem.kt | 14 +- .../archive/ArchiveFileSystemProvider.kt | 4 +- .../ArchivePasswordRequiredException.kt | 28 ++- .../archive/LocalArchiveFileSystem.kt | 28 ++- .../provider/archive/PathArchiveExtensions.kt | 10 + .../archive/RootArchiveFileAttributeView.kt | 2 +- .../provider/archive/RootArchiveFileSystem.kt | 32 ++- .../archive/RootArchiveFileSystemProvider.kt | 12 +- .../archive/archiver/ArchiveReader.kt | 20 +- .../provider/archive/archiver/ReadArchive.kt | 15 +- .../common/UserActionRequiredException.kt | 10 +- .../provider/remote/RemoteFileService.kt | 5 + .../remote/RemoteFileServiceInterface.kt | 5 + .../files/provider/remote/RemoteFileSystem.kt | 7 - .../files/provider/root/RootableFileSystem.kt | 6 - ...AllowSoftInputHackAlertDialogCustomView.kt | 29 +++ .../res/layout/archive_password_dialog.xml | 30 +++ ...iew.xml => file_job_error_dialog_view.xml} | 0 app/src/main/res/values-zh-rCN/strings.xml | 4 + app/src/main/res/values-zh-rTW/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 33 files changed, 631 insertions(+), 180 deletions(-) create mode 100644 app/src/main/java/me/zhanghai/android/files/fileaction/ArchivePasswordDialogActivity.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/fileaction/ArchivePasswordDialogFragment.kt rename app/src/main/java/me/zhanghai/android/files/filejob/{FileJobAction.kt => FileJobErrorAction.kt} (85%) rename app/src/main/java/me/zhanghai/android/files/filejob/{FileJobActionDialogActivity.kt => FileJobErrorDialogActivity.kt} (67%) rename app/src/main/java/me/zhanghai/android/files/filejob/{FileJobActionDialogFragment.kt => FileJobErrorDialogFragment.kt} (83%) rename app/src/main/java/me/zhanghai/android/files/filejob/{FileJobActionViewModel.kt => FileJobErrorViewModel.kt} (97%) create mode 100644 app/src/main/java/me/zhanghai/android/files/ui/AllowSoftInputHackAlertDialogCustomView.kt create mode 100644 app/src/main/res/layout/archive_password_dialog.xml rename app/src/main/res/layout/{file_job_action_dialog_view.xml => file_job_error_dialog_view.xml} (100%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7b28e0f8..ac67576d3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -275,7 +275,12 @@ android:theme="@style/Theme.MaterialFiles" /> + + diff --git a/app/src/main/aidl/me/zhanghai/android/files/provider/remote/IRemoteFileService.aidl b/app/src/main/aidl/me/zhanghai/android/files/provider/remote/IRemoteFileService.aidl index 85ffdb094..5d9dfba72 100644 --- a/app/src/main/aidl/me/zhanghai/android/files/provider/remote/IRemoteFileService.aidl +++ b/app/src/main/aidl/me/zhanghai/android/files/provider/remote/IRemoteFileService.aidl @@ -17,5 +17,7 @@ interface IRemoteFileService { in ParcelableObject attributeView ); + void setArchivePasswords(in ParcelableObject fileSystem, in List passwords); + void refreshArchiveFileSystem(in ParcelableObject fileSystem); } diff --git a/app/src/main/java/me/zhanghai/android/files/fileaction/ArchivePasswordDialogActivity.kt b/app/src/main/java/me/zhanghai/android/files/fileaction/ArchivePasswordDialogActivity.kt new file mode 100644 index 000000000..9cd8aa1a7 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/fileaction/ArchivePasswordDialogActivity.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.fileaction + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.commit +import me.zhanghai.android.files.app.AppActivity +import me.zhanghai.android.files.util.args +import me.zhanghai.android.files.util.putArgs + +class ArchivePasswordDialogActivity : AppActivity() { + private val args by args() + + private lateinit var fragment: ArchivePasswordDialogFragment + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Calls ensureSubDecor(). + findViewById(android.R.id.content) + if (savedInstanceState == null) { + fragment = ArchivePasswordDialogFragment().putArgs(args) + supportFragmentManager.commit { + add(fragment, ArchivePasswordDialogFragment::class.java.name) + } + } else { + fragment = supportFragmentManager.findFragmentByTag( + ArchivePasswordDialogFragment::class.java.name + ) as ArchivePasswordDialogFragment + } + } + + override fun onDestroy() { + super.onDestroy() + + if (isFinishing) { + fragment.onFinish() + } + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/fileaction/ArchivePasswordDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileaction/ArchivePasswordDialogFragment.kt new file mode 100644 index 000000000..7acfda892 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/fileaction/ArchivePasswordDialogFragment.kt @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.fileaction + +import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.util.SparseArray +import android.view.WindowManager +import android.widget.LinearLayout +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.core.widget.NestedScrollView +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java8.nio.file.Path +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.WriteWith +import me.zhanghai.android.files.R +import me.zhanghai.android.files.compat.requireViewByIdCompat +import me.zhanghai.android.files.databinding.ArchivePasswordDialogBinding +import me.zhanghai.android.files.provider.archive.archiveAddPassword +import me.zhanghai.android.files.provider.archive.archiveFile +import me.zhanghai.android.files.ui.AllowSoftInputHackAlertDialogCustomView +import me.zhanghai.android.files.util.ParcelableArgs +import me.zhanghai.android.files.util.ParcelableParceler +import me.zhanghai.android.files.util.ParcelableState +import me.zhanghai.android.files.util.RemoteCallback +import me.zhanghai.android.files.util.args +import me.zhanghai.android.files.util.finish +import me.zhanghai.android.files.util.getArgs +import me.zhanghai.android.files.util.getState +import me.zhanghai.android.files.util.hideTextInputLayoutErrorOnTextChange +import me.zhanghai.android.files.util.layoutInflater +import me.zhanghai.android.files.util.putArgs +import me.zhanghai.android.files.util.putState +import me.zhanghai.android.files.util.readParcelable +import me.zhanghai.android.files.util.setOnEditorConfirmActionListener + +class ArchivePasswordDialogFragment : AppCompatDialogFragment() { + private val args by args() + + private lateinit var binding: ArchivePasswordDialogBinding + + private var isListenerNotified = false + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + val hierarchyState = SparseArray() + .apply { binding.root.saveHierarchyState(this) } + outState.putState(State(hierarchyState)) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + return MaterialAlertDialogBuilder(requireContext(), theme) + .setTitle(getTitle(context)) + .setMessage(getMessage(args.path.archiveFile.fileName, context)) + .apply { + binding = ArchivePasswordDialogBinding.inflate(context.layoutInflater) + binding.passwordEdit.hideTextInputLayoutErrorOnTextChange(binding.passwordLayout) + binding.passwordEdit.setOnEditorConfirmActionListener { onOk() } + if (savedInstanceState != null) { + val state = savedInstanceState.getState() + binding.root.restoreHierarchyState(state.hierarchyState) + } + setView(AllowSoftInputHackAlertDialogCustomView(context)) + } + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(android.R.string.cancel) { _, _ -> finish() } + .create() + .apply { + setCanceledOnTouchOutside(false) + // Override the listener here so that we have control over when to close the dialog. + setOnShowListener { + getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { onOk() } + } + window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + } + } + + override fun onStart() { + super.onStart() + + val dialog = requireDialog() as AlertDialog + if (binding.root.parent == null) { + val scrollView = dialog.requireViewByIdCompat(R.id.scrollView) + val linearLayout = scrollView.getChildAt(0) as LinearLayout + linearLayout.addView(binding.root) + binding.passwordEdit.requestFocus() + } + } + + private fun onOk() { + val password = binding.passwordEdit.text!!.toString() + if (password.isEmpty()) { + binding.passwordLayout.error = + getString(R.string.file_action_archive_password_error_empty) + return + } + args.path.archiveAddPassword(password) + notifyListenerOnce(true) + finish() + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + notifyListenerOnce(false) + finish() + } + + fun onFinish() { + notifyListenerOnce(false) + } + + private fun notifyListenerOnce(successful: Boolean) { + if (isListenerNotified) { + return + } + args.listener(successful) + isListenerNotified = true + } + + companion object { + fun getTitle(context: Context): String = + context.getString(R.string.file_action_archive_password_title) + + fun getMessage(archiveFile: Path, context: Context): String = + context.getString( + R.string.file_action_archive_password_message_format, archiveFile.fileName + ) + } + + @Parcelize + class Args( + val path: @WriteWith Path, + val listener: @WriteWith() + (Boolean) -> Unit + ) : ParcelableArgs { + object ListenerParceler : Parceler<(Boolean) -> Unit> { + override fun create(parcel: Parcel): (Boolean) -> Unit = + parcel.readParcelable()!!.let { + { successful -> + it.sendResult(Bundle().putArgs(ListenerArgs(successful))) + } + } + + override fun ((Boolean) -> Unit).write(parcel: Parcel, flags: Int) { + parcel.writeParcelable( + RemoteCallback { + val args = it.getArgs() + this(args.successful) + }, flags + ) + } + + @Parcelize + private class ListenerArgs( + val successful: Boolean + ) : ParcelableArgs + } + } + + @Parcelize + private class State( + val hierarchyState: SparseArray + ) : ParcelableState +} diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogActivity.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogActivity.kt index 8ddf342fe..334aa36c0 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogActivity.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogActivity.kt @@ -5,12 +5,10 @@ package me.zhanghai.android.files.filejob -import android.content.Context import android.os.Bundle import android.view.View import androidx.fragment.app.commit import me.zhanghai.android.files.app.AppActivity -import me.zhanghai.android.files.file.FileItem import me.zhanghai.android.files.util.args import me.zhanghai.android.files.util.putArgs @@ -43,16 +41,4 @@ class FileJobConflictDialogActivity : AppActivity() { fragment.onFinish() } } - - companion object { - fun getTitle(sourceFile: FileItem, targetFile: FileItem, context: Context): String = - FileJobConflictDialogFragment.getTitle(sourceFile, targetFile, context) - - fun getMessage( - sourceFile: FileItem, - targetFile: FileItem, - type: CopyMoveType, - context: Context - ): String = FileJobConflictDialogFragment.getMessage(sourceFile, targetFile, type, context) - } } diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt index 5cebbe815..76fbe2285 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobConflictDialogFragment.kt @@ -352,5 +352,7 @@ class FileJobConflictDialogFragment : AppCompatDialogFragment() { } @Parcelize - private class State(val isAllChecked: Boolean) : ParcelableState + private class State( + val isAllChecked: Boolean + ) : ParcelableState } diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobAction.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorAction.kt similarity index 85% rename from app/src/main/java/me/zhanghai/android/files/filejob/FileJobAction.kt rename to app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorAction.kt index 117d5a6a2..e8b9a1a36 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobAction.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorAction.kt @@ -5,7 +5,7 @@ package me.zhanghai.android.files.filejob -enum class FileJobAction { +enum class FileJobErrorAction { POSITIVE, NEGATIVE, NEUTRAL, diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionDialogActivity.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorDialogActivity.kt similarity index 67% rename from app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionDialogActivity.kt rename to app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorDialogActivity.kt index 2c1c6a589..942393184 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionDialogActivity.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorDialogActivity.kt @@ -12,10 +12,10 @@ import me.zhanghai.android.files.app.AppActivity import me.zhanghai.android.files.util.args import me.zhanghai.android.files.util.putArgs -class FileJobActionDialogActivity : AppActivity() { - private val args by args() +class FileJobErrorDialogActivity : AppActivity() { + private val args by args() - private lateinit var fragment: FileJobActionDialogFragment + private lateinit var fragment: FileJobErrorDialogFragment override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -23,14 +23,14 @@ class FileJobActionDialogActivity : AppActivity() { // Calls ensureSubDecor(). findViewById(android.R.id.content) if (savedInstanceState == null) { - fragment = FileJobActionDialogFragment().putArgs(args) + fragment = FileJobErrorDialogFragment().putArgs(args) supportFragmentManager.commit { - add(fragment, FileJobActionDialogFragment::class.java.name) + add(fragment, FileJobErrorDialogFragment::class.java.name) } } else { fragment = supportFragmentManager.findFragmentByTag( - FileJobActionDialogFragment::class.java.name - ) as FileJobActionDialogFragment + FileJobErrorDialogFragment::class.java.name + ) as FileJobErrorDialogFragment } } diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorDialogFragment.kt similarity index 83% rename from app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionDialogFragment.kt rename to app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorDialogFragment.kt index efcb910d7..18e30481b 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorDialogFragment.kt @@ -22,7 +22,7 @@ import kotlinx.parcelize.Parcelize import kotlinx.parcelize.WriteWith import me.zhanghai.android.files.R import me.zhanghai.android.files.compat.requireViewByIdCompat -import me.zhanghai.android.files.databinding.FileJobActionDialogViewBinding +import me.zhanghai.android.files.databinding.FileJobErrorDialogViewBinding import me.zhanghai.android.files.provider.common.PosixFileStore import me.zhanghai.android.files.util.ActionState import me.zhanghai.android.files.util.ParcelableArgs @@ -42,12 +42,12 @@ import me.zhanghai.android.files.util.readParcelable import me.zhanghai.android.files.util.showToast import me.zhanghai.android.files.util.viewModels -class FileJobActionDialogFragment : AppCompatDialogFragment() { +class FileJobErrorDialogFragment : AppCompatDialogFragment() { private val args by args() - private val viewModel by viewModels { { FileJobActionViewModel() } } + private val viewModel by viewModels { { FileJobErrorViewModel() } } - private lateinit var binding: FileJobActionDialogViewBinding + private lateinit var binding: FileJobErrorDialogViewBinding private var isListenerNotified = false @@ -62,7 +62,7 @@ class FileJobActionDialogFragment : AppCompatDialogFragment() { .setTitle(args.title) .setMessage(args.message) .apply { - binding = FileJobActionDialogViewBinding.inflate(context.layoutInflater) + binding = FileJobErrorDialogViewBinding.inflate(context.layoutInflater) val hasReadOnlyFileStore = args.readOnlyFileStore != null binding.remountButton.isVisible = hasReadOnlyFileStore if (hasReadOnlyFileStore) { @@ -118,9 +118,9 @@ class FileJobActionDialogFragment : AppCompatDialogFragment() { private fun onDialogButtonClick(dialog: DialogInterface, which: Int) { val action = when (which) { - DialogInterface.BUTTON_POSITIVE -> FileJobAction.POSITIVE - DialogInterface.BUTTON_NEGATIVE -> FileJobAction.NEGATIVE - DialogInterface.BUTTON_NEUTRAL -> FileJobAction.NEUTRAL + DialogInterface.BUTTON_POSITIVE -> FileJobErrorAction.POSITIVE + DialogInterface.BUTTON_NEGATIVE -> FileJobErrorAction.NEGATIVE + DialogInterface.BUTTON_NEUTRAL -> FileJobErrorAction.NEUTRAL else -> throw AssertionError(which) } notifyListenerOnce(action, args.showAll && binding.allCheck.isChecked) @@ -141,15 +141,15 @@ class FileJobActionDialogFragment : AppCompatDialogFragment() { override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) - notifyListenerOnce(FileJobAction.CANCELED, false) + notifyListenerOnce(FileJobErrorAction.CANCELED, false) finish() } fun onFinish() { - notifyListenerOnce(FileJobAction.CANCELED, false) + notifyListenerOnce(FileJobErrorAction.CANCELED, false) } - private fun notifyListenerOnce(action: FileJobAction, isAll: Boolean) { + private fun notifyListenerOnce(action: FileJobErrorAction, isAll: Boolean) { if (isListenerNotified) { return } @@ -166,17 +166,17 @@ class FileJobActionDialogFragment : AppCompatDialogFragment() { val positiveButtonText: CharSequence?, val negativeButtonText: CharSequence?, val neutralButtonText: CharSequence?, - val listener: @WriteWith() (FileJobAction, Boolean) -> Unit + val listener: @WriteWith() (FileJobErrorAction, Boolean) -> Unit ) : ParcelableArgs { - object ListenerParceler : Parceler<(FileJobAction, Boolean) -> Unit> { - override fun create(parcel: Parcel): (FileJobAction, Boolean) -> Unit = + object ListenerParceler : Parceler<(FileJobErrorAction, Boolean) -> Unit> { + override fun create(parcel: Parcel): (FileJobErrorAction, Boolean) -> Unit = parcel.readParcelable()!!.let { { action, isAll -> it.sendResult(Bundle().putArgs(ListenerArgs(action, isAll))) } } - override fun ((FileJobAction, Boolean) -> Unit).write(parcel: Parcel, flags: Int) { + override fun ((FileJobErrorAction, Boolean) -> Unit).write(parcel: Parcel, flags: Int) { parcel.writeParcelable(RemoteCallback { val args = it.getArgs() this(args.action, args.isAll) @@ -185,12 +185,14 @@ class FileJobActionDialogFragment : AppCompatDialogFragment() { @Parcelize private class ListenerArgs( - val action: FileJobAction, + val action: FileJobErrorAction, val isAll: Boolean ) : ParcelableArgs } } @Parcelize - private class State(val isAllChecked: Boolean) : ParcelableState + private class State( + val isAllChecked: Boolean + ) : ParcelableState } diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionViewModel.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorViewModel.kt similarity index 97% rename from app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionViewModel.kt rename to app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorViewModel.kt index 1a835403d..aca3296ac 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobActionViewModel.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobErrorViewModel.kt @@ -17,7 +17,7 @@ import me.zhanghai.android.files.util.ActionState import me.zhanghai.android.files.util.isFinished import me.zhanghai.android.files.util.isReady -class FileJobActionViewModel : ViewModel() { +class FileJobErrorViewModel : ViewModel() { private val _remountState = MutableStateFlow>(ActionState.Ready()) val remountState = _remountState.asStateFlow() diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt index 91be7cc79..91b2f401d 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt @@ -51,6 +51,7 @@ import me.zhanghai.android.files.provider.common.PosixPrincipal import me.zhanghai.android.files.provider.common.PosixUser import me.zhanghai.android.files.provider.common.ProgressCopyOption import me.zhanghai.android.files.provider.common.ReadOnlyFileSystemException +import me.zhanghai.android.files.provider.common.UserActionRequiredException import me.zhanghai.android.files.provider.common.asByteStringListPath import me.zhanghai.android.files.provider.common.copyTo import me.zhanghai.android.files.provider.common.createDirectories @@ -467,8 +468,24 @@ private class TransferInfo(scanInfo: ScanInfo, val target: Path?) { } } +// TODO: Make invalid file name, remount etc user actions as well. @Throws(InterruptedIOException::class) -private fun FileJob.showActionDialog( +private fun FileJob.showUserAction(exception: UserActionRequiredException): Boolean = + try { + runBlocking { + suspendCoroutine { continuation -> + val userAction = exception.getUserAction(continuation, service) + BackgroundActivityStarter.startActivity( + userAction.intent, userAction.title, userAction.message, service + ) + } + } + } catch (e: InterruptedException) { + throw InterruptedIOException().apply { initCause(e) } + } + +@Throws(InterruptedIOException::class) +private fun FileJob.showErrorDialog( title: CharSequence, message: CharSequence, readOnlyFileStore: PosixFileStore?, @@ -476,17 +493,17 @@ private fun FileJob.showActionDialog( positiveButtonText: CharSequence?, negativeButtonText: CharSequence?, neutralButtonText: CharSequence? -): ActionResult = +): ErrorResult = try { runBlocking { suspendCoroutine { continuation -> BackgroundActivityStarter.startActivity( - FileJobActionDialogActivity::class.createIntent().putArgs( - FileJobActionDialogFragment.Args( + FileJobErrorDialogActivity::class.createIntent().putArgs( + FileJobErrorDialogFragment.Args( title, message, readOnlyFileStore, showAll, positiveButtonText, negativeButtonText, neutralButtonText ) { action, isAll -> - continuation.resume(ActionResult(action, isAll)) + continuation.resume(ErrorResult(action, isAll)) } ), title, message, service ) @@ -509,8 +526,8 @@ private fun FileJob.getReadOnlyFileStore(path: Path, exception: IOException): Po return if (fileStore.isReadOnly) fileStore else null } -private class ActionResult( - val action: FileJobAction, +private class ErrorResult( + val action: FileJobErrorAction, val isAll: Boolean ) @@ -530,8 +547,8 @@ private fun FileJob.showConflictDialog( ) { action, name, all -> continuation.resume(ConflictResult(action, name, all)) } - ), FileJobConflictDialogActivity.getTitle(sourceFile, targetFile, service), - FileJobConflictDialogActivity.getMessage(sourceFile, targetFile, type, service), + ), FileJobConflictDialogFragment.getTitle(sourceFile, targetFile, service), + FileJobConflictDialogFragment.getMessage(sourceFile, targetFile, type, service), service ) } @@ -674,7 +691,7 @@ private fun FileJob.archive( throw e } catch (e: IOException) { e.printStackTrace() - val result = showActionDialog( + val result = showErrorDialog( getString(R.string.file_job_archive_error_title_format, getFileName(file)), getString( R.string.file_job_archive_error_message_format, getFileName(archiveFile), @@ -687,7 +704,8 @@ private fun FileJob.archive( null ) when (result.action) { - FileJobAction.NEGATIVE, FileJobAction.CANCELED -> throw InterruptedIOException() + FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED -> + throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -879,7 +897,14 @@ private fun FileJob.create(path: Path, createDirectory: Boolean) { throw e } catch (e: IOException) { e.printStackTrace() - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString(R.string.file_job_create_error_title), getString( R.string.file_job_create_error_message_format, getFileName(path), e.toString() @@ -891,11 +916,12 @@ private fun FileJob.create(path: Path, createDirectory: Boolean) { null ) when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE, FileJobAction.CANCELED -> throw InterruptedIOException() + FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED -> + throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -973,7 +999,14 @@ private fun FileJob.delete(path: Path, transferInfo: TransferInfo?, actionAllInf } return } - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString(R.string.file_job_delete_error_title), getString( R.string.file_job_delete_error_message_format, getFileName(path), e.toString() @@ -985,11 +1018,11 @@ private fun FileJob.delete(path: Path, transferInfo: TransferInfo?, actionAllInf getString(android.R.string.cancel) ) when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE -> { + FileJobErrorAction.NEGATIVE -> { if (result.isAll) { actionAllInfo.skipDeleteError = true } @@ -999,14 +1032,14 @@ private fun FileJob.delete(path: Path, transferInfo: TransferInfo?, actionAllInf } return } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { if (transferInfo != null) { transferInfo.skipFileIgnoringSize() postDeleteNotification(transferInfo, path) } return } - FileJobAction.NEUTRAL -> throw InterruptedIOException() + FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -1153,7 +1186,7 @@ private fun FileJob.copyOrMove( postCopyMoveNotification(transferInfo, source, type) return false } - val result = showActionDialog( + val result = showErrorDialog( getString( type.getResourceId( R.string.file_job_cannot_copy_into_itself_title, @@ -1169,7 +1202,7 @@ private fun FileJob.copyOrMove( null ) return when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { if (result.isAll) { actionAllInfo.skipCopyMoveIntoItself = true } @@ -1177,12 +1210,12 @@ private fun FileJob.copyOrMove( postCopyMoveNotification(transferInfo, source, type) false } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { transferInfo.skipFile(source) postCopyMoveNotification(transferInfo, source, type) false } - FileJobAction.NEGATIVE -> throw InterruptedIOException() + FileJobErrorAction.NEGATIVE -> throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -1193,7 +1226,7 @@ private fun FileJob.copyOrMove( postCopyMoveNotification(transferInfo, source, type) return false } - val result = showActionDialog( + val result = showErrorDialog( getString( type.getResourceId( R.string.file_job_cannot_copy_over_itself_title, @@ -1209,7 +1242,7 @@ private fun FileJob.copyOrMove( null ) return when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { if (result.isAll) { actionAllInfo.skipCopyMoveOverItself = true } @@ -1217,12 +1250,12 @@ private fun FileJob.copyOrMove( postCopyMoveNotification(transferInfo, source, type) false } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { transferInfo.skipFile(source) postCopyMoveNotification(transferInfo, source, type) false } - FileJobAction.NEGATIVE -> throw InterruptedIOException() + FileJobErrorAction.NEGATIVE -> throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -1337,7 +1370,14 @@ private fun FileJob.copyOrMove( postCopyMoveNotification(transferInfo, source, type) return false } - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString( type.getResourceId( R.string.file_job_copy_error_title_format, @@ -1359,11 +1399,11 @@ private fun FileJob.copyOrMove( getString(android.R.string.cancel) ) return when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE -> { + FileJobErrorAction.NEGATIVE -> { if (result.isAll) { actionAllInfo.skipCopyMoveError = true } @@ -1371,12 +1411,12 @@ private fun FileJob.copyOrMove( postCopyMoveNotification(transferInfo, source, type) false } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { transferInfo.skipFile(source) postCopyMoveNotification(transferInfo, source, type) false } - FileJobAction.NEUTRAL -> throw InterruptedIOException() + FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() } } } while (retry) @@ -1501,7 +1541,14 @@ private fun FileJob.rename(path: Path, newPath: Path) { throw e } catch (e: IOException) { e.printStackTrace() - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString(R.string.file_job_rename_error_title_format, getFileName(path)), getString( R.string.file_job_rename_error_message_format, getFileName(newPath), @@ -1514,11 +1561,12 @@ private fun FileJob.rename(path: Path, newPath: Path) { null ) when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE, FileJobAction.CANCELED -> throw InterruptedIOException() + FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED -> + throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -1593,7 +1641,14 @@ private fun FileJob.restoreSeLinuxContext( postRestoreSeLinuxContextNotification(transferInfo, path) return } - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString(R.string.file_job_restore_selinux_context_error_title), getString( R.string.file_job_restore_selinux_context_error_message_format, @@ -1606,11 +1661,11 @@ private fun FileJob.restoreSeLinuxContext( getString(android.R.string.cancel) ) when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE -> { + FileJobErrorAction.NEGATIVE -> { if (result.isAll) { actionAllInfo.skipRestoreSeLinuxContextError = true } @@ -1618,12 +1673,12 @@ private fun FileJob.restoreSeLinuxContext( postRestoreSeLinuxContextNotification(transferInfo, path) return } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { transferInfo.skipFileIgnoringSize() postRestoreSeLinuxContextNotification(transferInfo, path) return } - FileJobAction.NEUTRAL -> throw InterruptedIOException() + FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -1710,7 +1765,14 @@ private fun FileJob.setGroup( postSetGroupNotification(transferInfo, path) return } - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString(R.string.file_job_set_group_error_title_format, getFileName(path)), getString( R.string.file_job_set_group_error_message_format, getPrincipalName(group), @@ -1723,11 +1785,11 @@ private fun FileJob.setGroup( getString(android.R.string.cancel) ) when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE -> { + FileJobErrorAction.NEGATIVE -> { if (result.isAll) { actionAllInfo.skipSetGroupError = true } @@ -1735,12 +1797,12 @@ private fun FileJob.setGroup( postSetGroupNotification(transferInfo, path) return } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { transferInfo.skipFileIgnoringSize() postSetGroupNotification(transferInfo, path) return } - FileJobAction.NEUTRAL -> throw InterruptedIOException() + FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -1849,7 +1911,14 @@ private fun FileJob.setMode( postSetModeNotification(transferInfo, path) return } - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString(R.string.file_job_set_mode_error_title_format, getFileName(path)), getString( R.string.file_job_set_mode_error_message_format, mode.toModeString(), @@ -1862,11 +1931,11 @@ private fun FileJob.setMode( getString(android.R.string.cancel) ) when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE -> { + FileJobErrorAction.NEGATIVE -> { if (result.isAll) { actionAllInfo.skipSetModeError = true } @@ -1874,12 +1943,12 @@ private fun FileJob.setMode( postSetModeNotification(transferInfo, path) return } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { transferInfo.skipFileIgnoringSize() postSetModeNotification(transferInfo, path) return } - FileJobAction.NEUTRAL -> throw InterruptedIOException() + FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -1962,7 +2031,14 @@ private fun FileJob.setOwner( postSetOwnerNotification(transferInfo, path) return } - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString(R.string.file_job_set_owner_error_title_format, getFileName(path)), getString( R.string.file_job_set_owner_error_message_format, getPrincipalName(owner), @@ -1975,11 +2051,11 @@ private fun FileJob.setOwner( getString(android.R.string.cancel) ) when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE -> { + FileJobErrorAction.NEGATIVE -> { if (result.isAll) { actionAllInfo.skipSetOwnerError = true } @@ -1987,12 +2063,12 @@ private fun FileJob.setOwner( postSetOwnerNotification(transferInfo, path) return } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { transferInfo.skipFileIgnoringSize() postSetOwnerNotification(transferInfo, path) return } - FileJobAction.NEUTRAL -> throw InterruptedIOException() + FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -2083,7 +2159,14 @@ private fun FileJob.setSeLinuxContext( postSetSeLinuxContextNotification(transferInfo, path) return } - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString( R.string.file_job_set_selinux_context_error_title_format, getFileName(path) ), @@ -2098,11 +2181,11 @@ private fun FileJob.setSeLinuxContext( getString(android.R.string.cancel) ) when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE -> { + FileJobErrorAction.NEGATIVE -> { if (result.isAll) { actionAllInfo.skipSetSeLinuxContextError = true } @@ -2110,12 +2193,12 @@ private fun FileJob.setSeLinuxContext( postSetSeLinuxContextNotification(transferInfo, path) return } - FileJobAction.CANCELED -> { + FileJobErrorAction.CANCELED -> { transferInfo.skipFileIgnoringSize() postSetSeLinuxContextNotification(transferInfo, path) return } - FileJobAction.NEUTRAL -> throw InterruptedIOException() + FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() else -> throw AssertionError(result.action) } } @@ -2167,7 +2250,14 @@ private fun FileJob.write(file: Path, content: ByteArray): Boolean { throw e } catch (e: IOException) { e.printStackTrace() - val result = showActionDialog( + if (e is UserActionRequiredException) { + val result = showUserAction(e) + if (result) { + retry = true + continue + } + } + val result = showErrorDialog( getString(R.string.file_job_write_error_title, getFileName(file)), getString( R.string.file_job_write_error_message_format, getFileName(file), e.toString() @@ -2179,12 +2269,12 @@ private fun FileJob.write(file: Path, content: ByteArray): Boolean { null ) return when (result.action) { - FileJobAction.POSITIVE -> { + FileJobErrorAction.POSITIVE -> { retry = true continue@loop } - FileJobAction.NEGATIVE, FileJobAction.CANCELED -> false - FileJobAction.NEUTRAL -> throw InterruptedIOException() + FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED -> false + FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() } } } while (retry) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt index 590f6ef33..f9613dd50 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt @@ -17,18 +17,15 @@ import java.io.InterruptedIOException // See also libarchive/archive_platform.h . private const val ARCHIVE_ERRNO_MISC = -1 -fun ArchiveException.toFileSystemOrInterruptedIOException( - file: String?, - other: String? = null -): IOException = +fun ArchiveException.toFileSystemOrInterruptedIOException(file: Path): IOException = when { // See also ReadArchive.toArchiveException . code == OsConstants.EINTR -> InterruptedIOException(message) // See also libarchive/archive_read_support_format_zip.c . code == ARCHIVE_ERRNO_MISC && ( message == "Incorrect passphrase" || message == "Passphrase required for this entry" - ) -> ArchivePasswordRequiredException(file, other, message) - else -> FileSystemException(file, other, message) + ) -> ArchivePasswordRequiredException(file, message) + else -> FileSystemException(file.toString(), null, message) }.apply { initCause(this@toFileSystemOrInterruptedIOException) } class ArchiveExceptionInputStream( @@ -40,7 +37,7 @@ class ArchiveExceptionInputStream( try { super.read() } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } @Throws(IOException::class) @@ -48,7 +45,7 @@ class ArchiveExceptionInputStream( try { super.read(b) } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } @Throws(IOException::class) @@ -56,14 +53,14 @@ class ArchiveExceptionInputStream( try { super.read(b, off, len) } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } @Throws(IOException::class) override fun skip(n: Long): Long = try { super.skip(n) } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } @Throws(IOException::class) @@ -71,7 +68,7 @@ class ArchiveExceptionInputStream( try { super.available() } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } @Throws(IOException::class) @@ -79,7 +76,7 @@ class ArchiveExceptionInputStream( try { super.close() } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } } @@ -88,7 +85,7 @@ class ArchiveExceptionInputStream( try { super.reset() } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt index 46252ddd1..dcbc4953d 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystem.kt @@ -51,14 +51,24 @@ internal class ArchiveFileSystem( @Throws(IOException::class) fun readSymbolicLinkAsLocal(link: Path): String = localFileSystem.readSymbolicLink(link) + fun addPassword(password: String) { + localFileSystem.addPassword(password) + rootFileSystem.addPassword(password) + } + + fun setPasswords(passwords: List) { + localFileSystem.setPasswords(passwords) + rootFileSystem.setPasswords(passwords) + } + fun refresh() { localFileSystem.refresh() rootFileSystem.refresh() } @Throws(RemoteFileSystemException::class) - fun doRefreshIfNeededAsRoot() { - rootFileSystem.doRefreshIfNeeded() + fun prepareAsRoot() { + rootFileSystem.prepare() } override fun getPath(first: ByteString, vararg more: ByteString): ArchivePath = diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystemProvider.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystemProvider.kt index 7f9e2c890..dba8e6d0b 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystemProvider.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileSystemProvider.kt @@ -29,7 +29,7 @@ object ArchiveFileSystemProvider : RootableFileSystemProvider( internal fun supportsFileAttributeView(type: Class): Boolean = LocalArchiveFileSystemProvider.supportsFileAttributeView(type) - internal fun doRefreshIfNeeded(path: Path) { - rootProvider.doRefreshIfNeeded(path) + internal fun prepareFileSystem(path: Path) { + rootProvider.prepareFileSystem(path) } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt index 1572b9a48..5ffe81da2 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt @@ -5,13 +5,29 @@ package me.zhanghai.android.files.provider.archive -import android.content.Intent +import android.content.Context +import java8.nio.file.Path +import me.zhanghai.android.files.fileaction.ArchivePasswordDialogActivity +import me.zhanghai.android.files.fileaction.ArchivePasswordDialogFragment +import me.zhanghai.android.files.provider.common.UserAction import me.zhanghai.android.files.provider.common.UserActionRequiredException +import me.zhanghai.android.files.util.createIntent +import me.zhanghai.android.files.util.putArgs +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume -class ArchivePasswordRequiredException : UserActionRequiredException { - constructor(file: String?) : super(file) +class ArchivePasswordRequiredException( + private val file: Path, + reason: String? +) : + UserActionRequiredException(file.toString(), null, reason) { - constructor(file: String?, other: String?, reason: String?) : super(file, other, reason) - - override fun getUserAction(): Intent = TODO() + override fun getUserAction(continuation: Continuation, context: Context): UserAction { + return UserAction( + ArchivePasswordDialogActivity::class.createIntent().putArgs( + ArchivePasswordDialogFragment.Args(file) { continuation.resume(it) } + ), ArchivePasswordDialogFragment.getTitle(context), + ArchivePasswordDialogFragment.getMessage(file, context) + ) + } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt index d099e0527..37f50288c 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt @@ -50,6 +50,8 @@ internal class LocalArchiveFileSystem( private var isOpen = true + private var passwords = listOf() + private var isRefreshNeeded = true private var entries: Map? = null @@ -78,9 +80,9 @@ internal class LocalArchiveFileSystem( throw IsDirectoryException(file.toString()) } val inputStream = try { - ArchiveReader.newInputStream(archiveFile, entry) + ArchiveReader.newInputStream(archiveFile, passwords, entry) } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } ?: throw NoSuchFileException(file.toString()) ArchiveExceptionInputStream(inputStream, file) } @@ -107,6 +109,24 @@ internal class LocalArchiveFileSystem( entry.symbolicLinkTarget ?: "" } + fun addPassword(password: String) { + synchronized(lock) { + if (!isOpen) { + throw ClosedFileSystemException() + } + passwords += password + } + } + + fun setPasswords(passwords: List) { + synchronized(lock) { + if (!isOpen) { + throw ClosedFileSystemException() + } + this.passwords = passwords + } + } + fun refresh() { synchronized(lock) { if (!isOpen) { @@ -123,9 +143,9 @@ internal class LocalArchiveFileSystem( } if (isRefreshNeeded) { val entriesAndTree = try { - ArchiveReader.readEntries(archiveFile, rootDirectory) + ArchiveReader.readEntries(archiveFile, passwords, rootDirectory) } catch (e: ArchiveException) { - throw e.toFileSystemOrInterruptedIOException(file.toString()) + throw e.toFileSystemOrInterruptedIOException(file) } entries = entriesAndTree.first tree = entriesAndTree.second diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/PathArchiveExtensions.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/PathArchiveExtensions.kt index 511744e2d..6c765e822 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/PathArchiveExtensions.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/PathArchiveExtensions.kt @@ -8,6 +8,16 @@ package me.zhanghai.android.files.provider.archive import java8.nio.file.Path import java8.nio.file.ProviderMismatchException +fun Path.archiveAddPassword(password: String) { + this as? ArchivePath ?: throw ProviderMismatchException(toString()) + fileSystem.addPassword(password) +} + +fun Path.archiveSetPasswords(passwords: List) { + this as? ArchivePath ?: throw ProviderMismatchException(toString()) + fileSystem.setPasswords(passwords) +} + val Path.archiveFile: Path get() { this as? ArchivePath ?: throw ProviderMismatchException(toString()) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileAttributeView.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileAttributeView.kt index fd5a7bf09..f04a0a17a 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileAttributeView.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileAttributeView.kt @@ -17,7 +17,7 @@ internal class RootArchiveFileAttributeView( ) : RootPosixFileAttributeView(attributeView) { @Throws(IOException::class) override fun readAttributes(): PosixFileAttributes { - ArchiveFileSystemProvider.doRefreshIfNeeded(path) + ArchiveFileSystemProvider.prepareFileSystem(path) return super.readAttributes() } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileSystem.kt index 002a60825..33466ffe1 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileSystem.kt @@ -13,25 +13,43 @@ import me.zhanghai.android.files.provider.root.RootFileSystem internal class RootArchiveFileSystem( private val fileSystem: FileSystem ) : RootFileSystem(fileSystem) { + private var passwords = listOf() + + private var isSetPasswordNeeded = false + private var isRefreshNeeded = false private val lock = Any() + fun addPassword(password: String) { + synchronized(lock) { + passwords += password + isSetPasswordNeeded = true + } + } + + fun setPasswords(passwords: List) { + synchronized(lock) { + this.passwords = passwords + isSetPasswordNeeded = true + } + } + fun refresh() { synchronized(lock) { - if (hasRemoteInterface()) { - isRefreshNeeded = true - } + isRefreshNeeded = true } } @Throws(RemoteFileSystemException::class) - fun doRefreshIfNeeded() { + fun prepare() { synchronized(lock) { + if (isSetPasswordNeeded) { + RootFileService.setArchivePasswords(fileSystem, passwords) + isSetPasswordNeeded = false + } if (isRefreshNeeded) { - if (hasRemoteInterface()) { - RootFileService.refreshArchiveFileSystem(fileSystem) - } + RootFileService.refreshArchiveFileSystem(fileSystem) isRefreshNeeded = false } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileSystemProvider.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileSystemProvider.kt index f8ef31081..89725857c 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileSystemProvider.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/RootArchiveFileSystemProvider.kt @@ -44,16 +44,8 @@ class RootArchiveFileSystemProvider(scheme: String) : RootFileSystemProvider(sch } @Throws(RemoteFileSystemException::class) - private fun prepareFileSystem(path: Path) { + internal fun prepareFileSystem(path: Path) { path as? ArchivePath ?: throw ProviderMismatchException(path.toString()) - val fileSystem = path.fileSystem - fileSystem.ensureRootInterface() - fileSystem.doRefreshIfNeededAsRoot() - } - - @Throws(RemoteFileSystemException::class) - internal fun doRefreshIfNeeded(path: Path) { - path as? ArchivePath ?: throw ProviderMismatchException(path.toString()) - path.fileSystem.doRefreshIfNeededAsRoot() + path.fileSystem.prepareAsRoot() } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt index 329568204..66595c745 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ArchiveReader.kt @@ -31,10 +31,11 @@ object ArchiveReader { @Throws(IOException::class) fun readEntries( file: Path, + passwords: List, rootPath: Path ): Pair, Map>> { val entries = mutableMapOf() - val rawEntries = readEntries(file) + val rawEntries = readEntries(file, passwords) for (entry in rawEntries) { var path = rootPath.resolve(entry.name) // Normalize an absolute path to prevent path traversal attack. @@ -88,9 +89,9 @@ object ArchiveReader { } @Throws(IOException::class) - private fun readEntries(file: Path): List { + private fun readEntries(file: Path, passwords: List): List { val charset = archiveFileNameCharset - val (archive, closeable) = openArchive(file) + val (archive, closeable) = openArchive(file, passwords) return closeable.use { buildList { while (true) { @@ -101,9 +102,9 @@ object ArchiveReader { } @Throws(IOException::class) - fun newInputStream(file: Path, entry: ReadArchive.Entry): InputStream? { + fun newInputStream(file: Path, passwords: List, entry: ReadArchive.Entry): InputStream? { val charset = archiveFileNameCharset - val (archive, closeable) = openArchive(file) + val (archive, closeable) = openArchive(file, passwords) var successful = false return try { while (true) { @@ -127,7 +128,10 @@ object ArchiveReader { } @Throws(IOException::class) - private fun openArchive(file: Path): Pair { + private fun openArchive( + file: Path, + passwords: List + ): Pair { val channel = try { CacheSizeSeekableByteChannel(file.newByteChannel()) } catch (e: Exception) { @@ -137,7 +141,7 @@ object ArchiveReader { if (channel != null) { var successful = false try { - val archive = ReadArchive(channel) + val archive = ReadArchive(channel, passwords) successful = true return archive to ArchiveCloseable(archive, channel) } finally { @@ -149,7 +153,7 @@ object ArchiveReader { val inputStream = file.newInputStream() var successful = false try { - val archive = ReadArchive(inputStream) + val archive = ReadArchive(inputStream, passwords) successful = true return archive to ArchiveCloseable(archive, inputStream) } finally { diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt index dda766857..440785924 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/ReadArchive.kt @@ -30,7 +30,7 @@ class ReadArchive : Closeable { private val archive = Archive.readNew() @Throws(ArchiveException::class) - constructor(inputStream: InputStream) { + constructor(inputStream: InputStream, passwords: List) { var successful = false try { Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) @@ -59,6 +59,9 @@ class ReadArchive : Closeable { throw e.toArchiveException("InputStream.skip") } } + for (password in passwords) { + Archive.readAddPassphrase(archive, password.toByteArray()) + } Archive.readOpen1(archive) successful = true } finally { @@ -69,7 +72,7 @@ class ReadArchive : Closeable { } @Throws(ArchiveException::class) - constructor(channel: SeekableByteChannel) { + constructor(channel: SeekableByteChannel, passwords: List) { var successful = false try { Archive.setCharset(archive, StandardCharsets.UTF_8.name().toByteArray()) @@ -117,6 +120,9 @@ class ReadArchive : Closeable { } newPosition } + for (password in passwords) { + Archive.readAddPassphrase(archive, password.toByteArray()) + } Archive.readOpen1(archive) successful = true } finally { @@ -196,11 +202,6 @@ class ReadArchive : Closeable { @Throws(ArchiveException::class) fun newDataInputStream(): InputStream = DataInputStream() - @Throws(ArchiveException::class) - fun addPassword(password: String) { - Archive.readAddPassphrase(archive, password.toByteArray()) - } - @Throws(ArchiveException::class) override fun close() { Archive.readFree(archive) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/common/UserActionRequiredException.kt b/app/src/main/java/me/zhanghai/android/files/provider/common/UserActionRequiredException.kt index c95022446..32440ce4d 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/common/UserActionRequiredException.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/common/UserActionRequiredException.kt @@ -5,13 +5,21 @@ package me.zhanghai.android.files.provider.common +import android.content.Context import android.content.Intent import java8.nio.file.FileSystemException +import kotlin.coroutines.Continuation abstract class UserActionRequiredException : FileSystemException { constructor(file: String?) : super(file) constructor(file: String?, other: String?, reason: String?) : super(file, other, reason) - abstract fun getUserAction(): Intent + abstract fun getUserAction(continuation: Continuation, context: Context): UserAction } + +class UserAction( + val intent: Intent, + val title: String, + val message: String? +) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileService.kt b/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileService.kt index feacf24fc..63138906a 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileService.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileService.kt @@ -30,6 +30,11 @@ abstract class RemoteFileService(private val remoteInterface: RemoteInterface) { + remoteInterface.get().call { setArchivePasswords(fileSystem.toParcelable(), passwords) } + } + @Throws(RemoteFileSystemException::class) fun refreshArchiveFileSystem(fileSystem: FileSystem) { remoteInterface.get().call { refreshArchiveFileSystem(fileSystem.toParcelable()) } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileServiceInterface.kt b/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileServiceInterface.kt index 93c18f0fc..c6831d2eb 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileServiceInterface.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileServiceInterface.kt @@ -8,6 +8,7 @@ package me.zhanghai.android.files.provider.remote import java8.nio.file.FileSystem import me.zhanghai.android.files.provider.FileSystemProviders import me.zhanghai.android.files.provider.archive.archiveRefresh +import me.zhanghai.android.files.provider.archive.archiveSetPasswords open class RemoteFileServiceInterface : IRemoteFileService.Stub() { override fun getRemoteFileSystemProviderInterface(scheme: String): IRemoteFileSystemProvider = @@ -25,6 +26,10 @@ open class RemoteFileServiceInterface : IRemoteFileService.Stub() { ): IRemotePosixFileAttributeView = RemotePosixFileAttributeViewInterface(attributeView.value()) + override fun setArchivePasswords(fileSystem: ParcelableObject, passwords: List) { + fileSystem.value().getPath("").archiveSetPasswords(passwords) + } + override fun refreshArchiveFileSystem(fileSystem: ParcelableObject) { fileSystem.value().getPath("").archiveRefresh() } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileSystem.kt index 84eb44dea..a14da0f65 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/remote/RemoteFileSystem.kt @@ -18,11 +18,4 @@ abstract class RemoteFileSystem( } remoteInterface.get().call { exception -> close(exception) } } - - protected fun hasRemoteInterface(): Boolean = remoteInterface.has() - - @Throws(RemoteFileSystemException::class) - fun ensureRemoteInterface() { - remoteInterface.get() - } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/root/RootableFileSystem.kt b/app/src/main/java/me/zhanghai/android/files/provider/root/RootableFileSystem.kt index f1135b68d..b71b32bc9 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/root/RootableFileSystem.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/root/RootableFileSystem.kt @@ -13,7 +13,6 @@ import java8.nio.file.PathMatcher import java8.nio.file.WatchService import java8.nio.file.attribute.UserPrincipalLookupService import java8.nio.file.spi.FileSystemProvider -import me.zhanghai.android.files.provider.remote.RemoteFileSystemException import java.io.IOException abstract class RootableFileSystem( @@ -23,11 +22,6 @@ abstract class RootableFileSystem( protected open val localFileSystem: FileSystem = localFileSystemCreator(this) protected open val rootFileSystem: RootFileSystem = rootFileSystemCreator(this) - @Throws(RemoteFileSystemException::class) - fun ensureRootInterface() { - rootFileSystem.ensureRemoteInterface() - } - override fun provider(): FileSystemProvider = localFileSystem.provider() @Throws(IOException::class) diff --git a/app/src/main/java/me/zhanghai/android/files/ui/AllowSoftInputHackAlertDialogCustomView.kt b/app/src/main/java/me/zhanghai/android/files/ui/AllowSoftInputHackAlertDialogCustomView.kt new file mode 100644 index 000000000..3a5f27ef5 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/ui/AllowSoftInputHackAlertDialogCustomView.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.annotation.AttrRes +import androidx.annotation.StyleRes +import androidx.core.view.isGone + +class AllowSoftInputHackAlertDialogCustomView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, + @StyleRes defStyleRes: Int = 0 +) : View(context, attrs, defStyleAttr, defStyleRes) { + override fun onCheckIsTextEditor(): Boolean = true + + // Called once during ViewGroup.addView(). + override fun hasFocus(): Boolean { + // Makes hasCustomPanel false in AlertController.setupView(). + (parent.parent as View).isGone = true + return super.hasFocus() + } +} diff --git a/app/src/main/res/layout/archive_password_dialog.xml b/app/src/main/res/layout/archive_password_dialog.xml new file mode 100644 index 000000000..17c38f3fb --- /dev/null +++ b/app/src/main/res/layout/archive_password_dialog.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/file_job_action_dialog_view.xml b/app/src/main/res/layout/file_job_error_dialog_view.xml similarity index 100% rename from app/src/main/res/layout/file_job_action_dialog_view.xml rename to app/src/main/res/layout/file_job_error_dialog_view.xml diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4d68beaae..d9fdfa08b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -69,6 +69,10 @@ FTP 服务器 显示和控制 FTP 服务器 + 需要密码 + “%1$s”受密码保护。 + 密码不能为空 + 正在准备压缩 %1$,d 个文件(%2$s) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 612830b2f..9694ed0f0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -69,6 +69,10 @@ FTP 伺服器 顯示和控制 FTP 伺服器 + 需要密碼 + “%1$s”受密碼保護。 + 密碼不能為空 + 正在準備壓縮 %1$,d 個檔案(%2$s) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4c9632637..7d99bffff 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,6 +74,10 @@ FTP server Display and control the FTP server + Password required + “%1$s” is password-protected. + Password cannot be empty + Preparing to compress %1$,d file (%2$s) Preparing to compress %1$,d files (%2$s) From 7933532aeaf46c0769dcf22333c5d4e03b83a841 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 30 Sep 2023 19:45:48 -0700 Subject: [PATCH 158/326] [Feature] Support creating encrypted ZIP archives. Fixes: #1016 --- .../filelist/CreateArchiveDialogFragment.kt | 38 +++++++++++++++---- .../provider/archive/archiver/WriteArchive.kt | 4 ++ .../main/res/layout/create_archive_dialog.xml | 18 +++++++++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 56 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt index 15263dc32..08e10d092 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/CreateArchiveDialogFragment.kt @@ -12,7 +12,9 @@ import android.view.View import android.widget.EditText import android.widget.RadioGroup import androidx.annotation.StringRes +import androidx.core.view.isGone import androidx.fragment.app.Fragment +import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import kotlinx.parcelize.Parcelize import me.zhanghai.android.files.R @@ -23,6 +25,7 @@ import me.zhanghai.android.files.util.args import me.zhanghai.android.files.util.putArgs import me.zhanghai.android.files.util.setTextWithSelection import me.zhanghai.android.files.util.show +import me.zhanghai.android.files.util.takeIfNotEmpty import me.zhanghai.android.libarchive.Archive class CreateArchiveDialogFragment : FileNameDialogFragment() { @@ -50,6 +53,8 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() { } name?.let { binding.nameEdit.setTextWithSelection(it) } } + binding.typeGroup.setOnCheckedChangeListener { _, _ -> updatePasswordLayoutVisibility() } + updatePasswordLayoutVisibility() return dialog } @@ -61,23 +66,39 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() { override val name: String get() { - val extension = when (val typeId = binding.typeGroup.checkedRadioButtonId) { + val extension = when (val checkedId = binding.typeGroup.checkedRadioButtonId) { R.id.zipRadio -> "zip" R.id.tarXzRadio -> "tar.xz" R.id.sevenZRadio -> "7z" - else -> throw AssertionError(typeId) + else -> throw AssertionError(checkedId) } return "${super.name}.$extension" } + private val isPasswordSupported: Boolean + get() = when (val checkedId = binding.typeGroup.checkedRadioButtonId) { + R.id.zipRadio -> true + R.id.tarXzRadio, R.id.sevenZRadio -> false + else -> throw AssertionError(checkedId) + } + + private fun updatePasswordLayoutVisibility() { + binding.passwordLayout.isGone = !isPasswordSupported + } + override fun onOk(name: String) { - val (format, filter) = when (val typeId = binding.typeGroup.checkedRadioButtonId) { + val (format, filter) = when (val checkedId = binding.typeGroup.checkedRadioButtonId) { R.id.zipRadio -> Archive.FORMAT_ZIP to Archive.FILTER_NONE R.id.tarXzRadio -> Archive.FORMAT_TAR to Archive.FILTER_XZ R.id.sevenZRadio -> Archive.FORMAT_7ZIP to Archive.FILTER_NONE - else -> throw AssertionError(typeId) + else -> throw AssertionError(checkedId) + } + val password = if (isPasswordSupported) { + binding.passwordEdit.text!!.toString().takeIfNotEmpty() + } else { + null } - listener.archive(args.files, name, format, filter, null) + listener.archive(args.files, name, format, filter, password) } companion object { @@ -93,7 +114,9 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() { root: View, nameLayout: TextInputLayout, nameEdit: EditText, - val typeGroup: RadioGroup + val typeGroup: RadioGroup, + val passwordLayout: TextInputLayout, + val passwordEdit: TextInputEditText ) : NameDialogFragment.Binding(root, nameLayout, nameEdit) { companion object { fun inflate(inflater: LayoutInflater): Binding { @@ -101,7 +124,8 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() { val bindingRoot = binding.root val nameBinding = NameDialogNameIncludeBinding.bind(bindingRoot) return Binding( - bindingRoot, nameBinding.nameLayout, nameBinding.nameEdit, binding.typeGroup + bindingRoot, nameBinding.nameLayout, nameBinding.nameEdit, binding.typeGroup, + binding.passwordLayout, binding.passwordEdit ) } } diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt index 42781ef8a..c17424201 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/archiver/WriteArchive.kt @@ -37,7 +37,11 @@ class WriteArchive @Throws(ArchiveException::class) constructor( Archive.writeSetFormat(archive, format) Archive.writeAddFilter(archive, filter) if (password != null) { + require(format == Archive.FORMAT_ZIP) Archive.writeSetPassphrase(archive, password.toByteArray()) + Archive.writeSetFormatOption( + archive, null, "encryption".toByteArray(), "zipcrypt".toByteArray() + ) } Archive.writeOpen( archive, null, null, { _, _, buffer -> channel.write(buffer) }, null diff --git a/app/src/main/res/layout/create_archive_dialog.xml b/app/src/main/res/layout/create_archive_dialog.xml index a6cfeffb6..724e0a007 100644 --- a/app/src/main/res/layout/create_archive_dialog.xml +++ b/app/src/main/res/layout/create_archive_dialog.xml @@ -7,6 +7,7 @@ @@ -61,6 +62,23 @@ android:textAppearance="?textAppearanceListItem" android:visibility="@integer/create_archive_type_seven_z_visibility" /> + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d9fdfa08b..44d8225cf 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -230,6 +230,7 @@ .zip .tar.xz .7z + 密码(可选) 已添加书签 新建文件 新建文件夹 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 9694ed0f0..223befefa 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -230,6 +230,7 @@ .zip .tar.xz .7z + 密碼(選填) 已新增書籤 新檔案 新資料夾 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d99bffff..47efb246a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -259,6 +259,7 @@ .zip .tar.xz .7z + Password (optional) Bookmark added New file New folder From 20ded2806b668e5cc3b5f181a78842248f0c7df2 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 30 Sep 2023 19:50:05 -0700 Subject: [PATCH 159/326] [Refactor] Remove unnecessary loop labels in FileJobs. It was necessary because Java doesn't allow continue in switch statements, but that's no longer the case for Kotlin when. --- .../android/files/filejob/FileJobs.kt | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt index 91b2f401d..197bb73ed 100644 --- a/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt +++ b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobs.kt @@ -885,7 +885,7 @@ class CreateFileJob(private val path: Path, private val createDirectory: Boolean @Throws(IOException::class) private fun FileJob.create(path: Path, createDirectory: Boolean) { var retry: Boolean - loop@ do { + do { retry = false try { if (createDirectory) { @@ -918,7 +918,7 @@ private fun FileJob.create(path: Path, createDirectory: Boolean) { when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED -> throw InterruptedIOException() @@ -980,7 +980,7 @@ class DeleteFileJob(private val paths: List) : FileJob() { @Throws(IOException::class) private fun FileJob.delete(path: Path, transferInfo: TransferInfo?, actionAllInfo: ActionAllInfo) { var retry: Boolean - loop@ do { + do { retry = false try { path.delete() @@ -1020,7 +1020,7 @@ private fun FileJob.delete(path: Path, transferInfo: TransferInfo?, actionAllInf when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE -> { if (result.isAll) { @@ -1262,7 +1262,7 @@ private fun FileJob.copyOrMove( var target = target var replaceExisting = false var retry: Boolean - loop@ do { + do { retry = false val options = mutableListOf().apply { this += LinkOption.NOFOLLOW_LINKS @@ -1327,13 +1327,13 @@ private fun FileJob.copyOrMove( } else { replaceExisting = true retry = true - continue@loop + continue } } FileJobConflictAction.RENAME -> { target = target.resolveSibling(result.name) retry = true - continue@loop + continue } FileJobConflictAction.SKIP -> { if (result.isAll) { @@ -1401,7 +1401,7 @@ private fun FileJob.copyOrMove( return when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE -> { if (result.isAll) { @@ -1533,7 +1533,7 @@ class RenameFileJob(private val path: Path, private val newName: String) : FileJ @Throws(IOException::class) private fun FileJob.rename(path: Path, newPath: Path) { var retry: Boolean - loop@ do { + do { retry = false try { moveAtomically(path, newPath) @@ -1563,7 +1563,7 @@ private fun FileJob.rename(path: Path, newPath: Path) { when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED -> throw InterruptedIOException() @@ -1625,7 +1625,7 @@ private fun FileJob.restoreSeLinuxContext( actionAllInfo: ActionAllInfo ) { var retry: Boolean - loop@ do { + do { retry = false try { val options = if (followLinks) arrayOf() else arrayOf(LinkOption.NOFOLLOW_LINKS) @@ -1663,7 +1663,7 @@ private fun FileJob.restoreSeLinuxContext( when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE -> { if (result.isAll) { @@ -1749,7 +1749,7 @@ private fun FileJob.setGroup( actionAllInfo: ActionAllInfo ) { var retry: Boolean - loop@ do { + do { retry = false try { val options = if (followLinks) arrayOf() else arrayOf(LinkOption.NOFOLLOW_LINKS) @@ -1787,7 +1787,7 @@ private fun FileJob.setGroup( when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE -> { if (result.isAll) { @@ -1895,7 +1895,7 @@ private fun FileJob.setMode( actionAllInfo: ActionAllInfo ) { var retry: Boolean - loop@ do { + do { retry = false try { // This will always follow symbolic links. @@ -1933,7 +1933,7 @@ private fun FileJob.setMode( when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE -> { if (result.isAll) { @@ -2015,7 +2015,7 @@ private fun FileJob.setOwner( actionAllInfo: ActionAllInfo ) { var retry: Boolean - loop@ do { + do { retry = false try { val options = if (followLinks) arrayOf() else arrayOf(LinkOption.NOFOLLOW_LINKS) @@ -2053,7 +2053,7 @@ private fun FileJob.setOwner( when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE -> { if (result.isAll) { @@ -2143,7 +2143,7 @@ private fun FileJob.setSeLinuxContext( actionAllInfo: ActionAllInfo ) { var retry: Boolean - loop@ do { + do { retry = false try { val options = if (followLinks) arrayOf() else arrayOf(LinkOption.NOFOLLOW_LINKS) @@ -2183,7 +2183,7 @@ private fun FileJob.setSeLinuxContext( when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE -> { if (result.isAll) { @@ -2235,7 +2235,7 @@ private fun FileJob.write(file: Path, content: ByteArray): Boolean { addToSize(content.size.toLong()) } var retry: Boolean - loop@ do { + do { retry = false val transferInfo = TransferInfo(scanInfo, file) try { @@ -2271,7 +2271,7 @@ private fun FileJob.write(file: Path, content: ByteArray): Boolean { return when (result.action) { FileJobErrorAction.POSITIVE -> { retry = true - continue@loop + continue } FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED -> false FileJobErrorAction.NEUTRAL -> throw InterruptedIOException() From e96de05040a3661cc202dcb6774734b6f46008fd Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 30 Sep 2023 19:56:22 -0700 Subject: [PATCH 160/326] [Feature] Try harder to request necessary permissions. --- .../zhanghai/android/files/filelist/FileListFragment.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index 1fdbf5d4b..5f19e9ec1 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -1368,9 +1368,11 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. if (isGranted) { viewModel.isStorageAccessRequested = false refresh() - } else if (!shouldShowRequestPermissionRationale( + } else if (shouldShowRequestPermissionRationale( android.Manifest.permission.WRITE_EXTERNAL_STORAGE )) { + ShowRequestStoragePermissionRationaleDialogFragment.show(this) + } else { ShowRequestStoragePermissionInSettingsRationaleDialogFragment.show(this) } } @@ -1417,9 +1419,11 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. if (isGranted) { viewModel.isNotificationPermissionRequested = false refresh() - } else if (!shouldShowRequestPermissionRationale( + } else if (shouldShowRequestPermissionRationale( android.Manifest.permission.POST_NOTIFICATIONS )) { + ShowRequestNotificationPermissionRationaleDialogFragment.show(this) + } else { ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.show(this) } } From bb351bfb80015ecf113c1af52f9f36c6c8bb9ea3 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sat, 30 Sep 2023 20:32:07 -0700 Subject: [PATCH 161/326] [Feature] Much better permission request flow. Bug: #998 --- .../files/filelist/FileListFragment.kt | 83 ++++++++++++++----- ...stAllFilesAccessRationaleDialogFragment.kt | 17 +++- ...issionInSettingsRationaleDialogFragment.kt | 15 +++- ...cationPermissionRationaleDialogFragment.kt | 15 +++- ...issionInSettingsRationaleDialogFragment.kt | 15 +++- ...toragePermissionRationaleDialogFragment.kt | 17 +++- 6 files changed, 126 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index 5f19e9ec1..af75337df 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -331,7 +331,10 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. override fun onResume() { super.onResume() - if (ensureStorageAccess()) { + if (!viewModel.isNotificationPermissionRequested) { + ensureStorageAccess() + } + if (!viewModel.isStorageAccessRequested) { ensureNotificationPermission() } } @@ -516,10 +519,6 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. if (viewModel.navigateUp(false)) { return true } - // See also https://developer.android.com/about/versions/12/behavior-changes-all#back-press - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && requireActivity().isTaskRoot) { - viewModel.isStorageAccessRequested = false - } return false } @@ -1321,15 +1320,14 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. binding.drawerLayout?.closeDrawer(GravityCompat.START) } - private fun ensureStorageAccess(): Boolean { + private fun ensureStorageAccess() { if (viewModel.isStorageAccessRequested) { - return true + return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()) { ShowRequestAllFilesAccessRationaleDialogFragment.show(this) viewModel.isStorageAccessRequested = true - return false } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != @@ -1343,24 +1341,41 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. requestStoragePermission() } viewModel.isStorageAccessRequested = true - return false } } - return true } - override fun requestAllFilesAccess() { + override fun onShowRequestAllFilesAccessRationaleResult(shouldRequest: Boolean) { + if (shouldRequest) { + requestAllFilesAccess() + } else { + viewModel.isStorageAccessRequested = false + // This isn't an onActivityResult() callback so it's not delivered before calling + // onResume(), and we need to do this manually. + ensureNotificationPermission() + } + } + + private fun requestAllFilesAccess() { requestAllFilesAccessLauncher.launch(Unit) } private fun onRequestAllFilesAccessResult(isGranted: Boolean) { + viewModel.isStorageAccessRequested = false if (isGranted) { - viewModel.isStorageAccessRequested = false refresh() } } - override fun requestStoragePermission() { + override fun onShowRequestStoragePermissionRationaleResult(shouldRequest: Boolean) { + if (shouldRequest) { + requestStoragePermission() + } else { + viewModel.isStorageAccessRequested = false + } + } + + private fun requestStoragePermission() { requestStoragePermissionLauncher.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) } @@ -1377,20 +1392,28 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. } } - override fun requestStoragePermissionInSettings() { + override fun onShowRequestStoragePermissionInSettingsRationaleResult(shouldRequest: Boolean) { + if (shouldRequest) { + requestStoragePermissionInSettings() + } else { + viewModel.isStorageAccessRequested = false + } + } + + private fun requestStoragePermissionInSettings() { requestStoragePermissionInSettingsLauncher.launch(Unit) } private fun onRequestStoragePermissionInSettingsResult(isGranted: Boolean) { + viewModel.isStorageAccessRequested = false if (isGranted) { - viewModel.isStorageAccessRequested = false refresh() } } - private fun ensureNotificationPermission(): Boolean { + private fun ensureNotificationPermission() { if (viewModel.isNotificationPermissionRequested) { - return true + return } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != @@ -1403,14 +1426,21 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. requestNotificationPermission() } viewModel.isNotificationPermissionRequested = true - return false } } - return true } @RequiresApi(Build.VERSION_CODES.TIRAMISU) - override fun requestNotificationPermission() { + override fun onShowRequestNotificationPermissionRationaleResult(shouldRequest: Boolean) { + if (shouldRequest) { + requestNotificationPermission() + } else { + viewModel.isNotificationPermissionRequested = false + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun requestNotificationPermission() { requestNotificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) } @@ -1429,7 +1459,18 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. } @RequiresApi(Build.VERSION_CODES.TIRAMISU) - override fun requestNotificationPermissionInSettings() { + override fun onShowRequestNotificationPermissionInSettingsRationaleResult( + shouldRequest: Boolean + ) { + if (shouldRequest) { + requestNotificationPermissionInSettings() + } else { + viewModel.isNotificationPermissionRequested = false + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun requestNotificationPermissionInSettings() { requestNotificationPermissionInSettingsLauncher.launch(Unit) } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestAllFilesAccessRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestAllFilesAccessRationaleDialogFragment.kt index 5de8d49a1..07c502327 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestAllFilesAccessRationaleDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestAllFilesAccessRationaleDialogFragment.kt @@ -6,6 +6,7 @@ package me.zhanghai.android.files.filelist import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment import androidx.fragment.app.Fragment @@ -20,11 +21,21 @@ class ShowRequestAllFilesAccessRationaleDialogFragment : AppCompatDialogFragment override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return MaterialAlertDialogBuilder(requireContext(), theme) .setMessage(R.string.all_files_access_rationale_message) - .setPositiveButton(android.R.string.ok) { _, _ -> listener.requestAllFilesAccess() } - .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + listener.onShowRequestAllFilesAccessRationaleResult(true) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + listener.onShowRequestAllFilesAccessRationaleResult(false) + } .create() } + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + listener.onShowRequestAllFilesAccessRationaleResult(false) + } + companion object { fun show(fragment: Fragment) { ShowRequestAllFilesAccessRationaleDialogFragment().show(fragment) @@ -32,6 +43,6 @@ class ShowRequestAllFilesAccessRationaleDialogFragment : AppCompatDialogFragment } interface Listener { - fun requestAllFilesAccess() + fun onShowRequestAllFilesAccessRationaleResult(shouldRequest: Boolean) } } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt index 062dae144..2384fad33 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt @@ -6,6 +6,7 @@ package me.zhanghai.android.files.filelist import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment import androidx.fragment.app.Fragment @@ -21,12 +22,20 @@ class ShowRequestNotificationPermissionInSettingsRationaleDialogFragment : AppCo return MaterialAlertDialogBuilder(requireContext(), theme) .setMessage(R.string.notification_permission_rationale_message) .setPositiveButton(R.string.open_settings) { _, _ -> - listener.requestNotificationPermissionInSettings() + listener.onShowRequestNotificationPermissionInSettingsRationaleResult(true) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + listener.onShowRequestNotificationPermissionInSettingsRationaleResult(false) } - .setNegativeButton(android.R.string.cancel, null) .create() } + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + listener.onShowRequestNotificationPermissionInSettingsRationaleResult(false) + } + companion object { fun show(fragment: Fragment) { ShowRequestNotificationPermissionInSettingsRationaleDialogFragment().show(fragment) @@ -34,6 +43,6 @@ class ShowRequestNotificationPermissionInSettingsRationaleDialogFragment : AppCo } interface Listener { - fun requestNotificationPermissionInSettings() + fun onShowRequestNotificationPermissionInSettingsRationaleResult(shouldRequest: Boolean) } } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt index 66cc5cee5..1f589ad84 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt @@ -6,6 +6,7 @@ package me.zhanghai.android.files.filelist import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment import androidx.fragment.app.Fragment @@ -21,12 +22,20 @@ class ShowRequestNotificationPermissionRationaleDialogFragment : AppCompatDialog return MaterialAlertDialogBuilder(requireContext(), theme) .setMessage(R.string.notification_permission_rationale_message) .setPositiveButton(android.R.string.ok) { _, _ -> - listener.requestNotificationPermission() + listener.onShowRequestNotificationPermissionRationaleResult(true) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + listener.onShowRequestNotificationPermissionRationaleResult(false) } - .setNegativeButton(android.R.string.cancel, null) .create() } + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + listener.onShowRequestNotificationPermissionRationaleResult(false) + } + companion object { fun show(fragment: Fragment) { ShowRequestNotificationPermissionRationaleDialogFragment().show(fragment) @@ -34,6 +43,6 @@ class ShowRequestNotificationPermissionRationaleDialogFragment : AppCompatDialog } interface Listener { - fun requestNotificationPermission() + fun onShowRequestNotificationPermissionRationaleResult(shouldRequest: Boolean) } } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestStoragePermissionInSettingsRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestStoragePermissionInSettingsRationaleDialogFragment.kt index 64409c647..28b4d0faf 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestStoragePermissionInSettingsRationaleDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestStoragePermissionInSettingsRationaleDialogFragment.kt @@ -6,6 +6,7 @@ package me.zhanghai.android.files.filelist import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment import androidx.fragment.app.Fragment @@ -21,12 +22,20 @@ class ShowRequestStoragePermissionInSettingsRationaleDialogFragment : AppCompatD return MaterialAlertDialogBuilder(requireContext(), theme) .setMessage(R.string.storage_permission_rationale_message) .setPositiveButton(R.string.open_settings) { _, _ -> - listener.requestStoragePermissionInSettings() + listener.onShowRequestStoragePermissionInSettingsRationaleResult(true) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + listener.onShowRequestStoragePermissionInSettingsRationaleResult(false) } - .setNegativeButton(android.R.string.cancel, null) .create() } + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + listener.onShowRequestStoragePermissionInSettingsRationaleResult(false) + } + companion object { fun show(fragment: Fragment) { ShowRequestStoragePermissionInSettingsRationaleDialogFragment().show(fragment) @@ -34,6 +43,6 @@ class ShowRequestStoragePermissionInSettingsRationaleDialogFragment : AppCompatD } interface Listener { - fun requestStoragePermissionInSettings() + fun onShowRequestStoragePermissionInSettingsRationaleResult(shouldRequest: Boolean) } } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestStoragePermissionRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestStoragePermissionRationaleDialogFragment.kt index 1fe9e932c..afa589800 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestStoragePermissionRationaleDialogFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestStoragePermissionRationaleDialogFragment.kt @@ -6,6 +6,7 @@ package me.zhanghai.android.files.filelist import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import androidx.appcompat.app.AppCompatDialogFragment import androidx.fragment.app.Fragment @@ -20,11 +21,21 @@ class ShowRequestStoragePermissionRationaleDialogFragment : AppCompatDialogFragm override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return MaterialAlertDialogBuilder(requireContext(), theme) .setMessage(R.string.storage_permission_rationale_message) - .setPositiveButton(android.R.string.ok) { _, _ -> listener.requestStoragePermission() } - .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + listener.onShowRequestStoragePermissionRationaleResult(true) + } + .setNegativeButton(android.R.string.cancel) { _, _ -> + listener.onShowRequestStoragePermissionRationaleResult(false) + } .create() } + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + listener.onShowRequestStoragePermissionRationaleResult(false) + } + companion object { fun show(fragment: Fragment) { ShowRequestStoragePermissionRationaleDialogFragment().show(fragment) @@ -32,6 +43,6 @@ class ShowRequestStoragePermissionRationaleDialogFragment : AppCompatDialogFragm } interface Listener { - fun requestStoragePermission() + fun onShowRequestStoragePermissionRationaleResult(shouldRequest: Boolean) } } From f6fa2f875a7623a833097d341338c340d3cab8d5 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 1 Oct 2023 02:29:03 -0700 Subject: [PATCH 162/326] [Fix] Fix Sui not working with R8 full mode. libsu has: -keep,allowobfuscation class * extends com.topjohnwu.superuser.ipc.RootService { *; } However since Shizuku-API doesn't use a particular base class and only requires the class to be extending IBinder, there's no good proguard rule that can be bundled within that library. So just mark our class with @Keep for simplicity. Fixes: #1034 --- .../android/files/provider/root/SuiFileServiceLauncher.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/root/SuiFileServiceLauncher.kt b/app/src/main/java/me/zhanghai/android/files/provider/root/SuiFileServiceLauncher.kt index d32ac55dd..a8a779001 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/root/SuiFileServiceLauncher.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/root/SuiFileServiceLauncher.kt @@ -11,6 +11,7 @@ import android.content.pm.PackageManager import android.os.Build import android.os.IBinder import androidx.annotation.ChecksSdkIntAtLeast +import androidx.annotation.Keep import androidx.annotation.RequiresApi import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.runBlocking @@ -145,6 +146,7 @@ object SuiFileServiceLauncher { } } +@Keep @RequiresApi(Build.VERSION_CODES.M) class SuiFileServiceInterface : RemoteFileServiceInterface() { init { From bd9df0ea76de1c776fd09ec3cae3acbbbd941574 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 1 Oct 2023 03:01:22 -0700 Subject: [PATCH 163/326] [Feature] Update Bouncy Castle to remove ~4MB of unused resource files. Those files are related to the now defunct PQC SIKE algorithm. See also https://www.bouncycastle.org/releasenotes.html#r1rv74 . --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 319278029..3a586fbd9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -178,7 +178,7 @@ dependencies { // org.bouncycastle:bcprov-jdk15to18 instead. exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' } - implementation 'org.bouncycastle:bcprov-jdk15to18:1.73' + implementation 'org.bouncycastle:bcprov-jdk15to18:1.76' implementation platform('io.coil-kt:coil-bom:2.4.0') implementation 'io.coil-kt:coil' implementation 'io.coil-kt:coil-gif' From 04cce72847e3b80b87f685eeb836b845849c3472 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 1 Oct 2023 03:36:50 -0700 Subject: [PATCH 164/326] [Feature] Exclude more unneeded large resource files from Bouncy Castle. --- app/build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 3a586fbd9..25af2faef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,10 @@ android { useLegacyPackaging true } resources { - excludes += ['META-INF/DEPENDENCIES'] + excludes += [ + 'META-INF/DEPENDENCIES', + 'org/bouncycastle/pqc/crypto/picnic/*' + ] } } lint { From 02edc075eba8ec4e233bbb3cda1447a82e456322 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 1 Oct 2023 19:51:13 -0700 Subject: [PATCH 165/326] [Fix] No need to refresh after notification permission is granted. It was a copy paste error. --- .../java/me/zhanghai/android/files/filelist/FileListFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index af75337df..9dc566405 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -1448,7 +1448,6 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. private fun onRequestNotificationPermissionResult(isGranted: Boolean) { if (isGranted) { viewModel.isNotificationPermissionRequested = false - refresh() } else if (shouldShowRequestPermissionRationale( android.Manifest.permission.POST_NOTIFICATIONS )) { @@ -1478,7 +1477,6 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. private fun onRequestNotificationPermissionInSettingsResult(isGranted: Boolean) { if (isGranted) { viewModel.isNotificationPermissionRequested = false - refresh() } } From 3a975310138ff06b68d3d837f8a4c768fdd77163 Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Tue, 3 Oct 2023 04:14:55 -0700 Subject: [PATCH 166/326] [Fix] Fix display cutout theming. Display cutout value shortEdges is actually potentially available on API 27, and display cutout value always is available on API 30. See also https://android-developers.googleblog.com/2018/07/supporting-display-cutouts-on-edge-to.html about the potential backport to API 27. --- app/src/main/res/values-v27/themes.xml | 6 ++++++ .../main/res/values-v27/themes_material3.xml | 6 ++++++ app/src/main/res/values-v28/themes.xml | 19 ------------------- .../main/res/values-v28/themes_material3.xml | 19 ------------------- app/src/main/res/values-v29/themes.xml | 4 ++-- .../main/res/values-v29/themes_material3.xml | 4 ++-- app/src/main/res/values-v30/themes.xml | 19 +++++++++++++++++++ .../main/res/values-v30/themes_material3.xml | 19 +++++++++++++++++++ 8 files changed, 54 insertions(+), 42 deletions(-) delete mode 100644 app/src/main/res/values-v28/themes.xml delete mode 100644 app/src/main/res/values-v28/themes_material3.xml create mode 100644 app/src/main/res/values-v30/themes.xml create mode 100644 app/src/main/res/values-v30/themes_material3.xml diff --git a/app/src/main/res/values-v27/themes.xml b/app/src/main/res/values-v27/themes.xml index a22d421c1..c2c9b561e 100644 --- a/app/src/main/res/values-v27/themes.xml +++ b/app/src/main/res/values-v27/themes.xml @@ -9,7 +9,13 @@ + + - - - - + + + + + + diff --git a/app/src/main/res/values/themes_material3.xml b/app/src/main/res/values/themes_material3.xml index b9ab21449..427b8d9c1 100644 --- a/app/src/main/res/values/themes_material3.xml +++ b/app/src/main/res/values/themes_material3.xml @@ -99,4 +99,6 @@