diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index eae9afc13..a1c117e66 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -9,16 +9,18 @@ jobs:
steps:
- name: Check out repository
- uses: actions/checkout@v3
- - name: Set up JDK 11
- uses: actions/setup-java@v2
+ uses: actions/checkout@v4
with:
- distribution: 'adopt'
- java-version: '11'
+ submodules: true
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '21'
- name: Build with Gradle
run: ./gradlew assembleDebug lintVitalRelease
- name: Upload artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: app-debug.apk
path: app/build/outputs/apk/debug/app-debug.apk
diff --git a/.gitignore b/.gitignore
index 256df604e..63a656514 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
/.gradle/
/.idea/
+/.kotlin/
/build/
/captures/
/local.properties
diff --git a/README.md b/README.md
index b41cfdda5..81ae655a4 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,9 @@
An open source Material Design file manager, for Android 5.0+.
-[
](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) [
](https://f-droid.org/packages/me.zhanghai.android.files)
+[
](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) [
](https://f-droid.org/packages/me.zhanghai.android.files) [
](https://github.com/zhanghai/MaterialFiles/releases/latest/download/app-release-universal.apk)
-[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/), [Microsoft language resources](https://learn.microsoft.com/en-us/globalization/reference/microsoft-language-resources), [MIME type translations](https://gitlab.freedesktop.org/xdg/shared-mime-info/-/tree/master/po))
## Preview
@@ -24,9 +22,9 @@ An open source Material Design file manager, for Android 5.0+.
- Breadcrumbs: Navigate in the filesystem with ease.
- Root support: View and manage files with root access.
- Archive support: View, extract and create common compressed files.
-- NAS support: View and manage files on FTP, SFTP and SMB servers.
+- NAS support: View and manage files on FTP, SFTP, SMB and WebDAV servers.
- Themes: Customizable UI colors, plus night mode with optional true black.
-- Linux-aware: Like [Nautilus](https://wiki.gnome.org/action/show/Apps/Files), knows symbolic links, file permissions and SELinux context.
+- Linux-aware: Like [Nautilus](https://apps.gnome.org/Nautilus/), knows symbolic links, file permissions and SELinux context.
- Robust: Uses Linux system calls under the hood, not yet another [`ls` parser](https://news.ycombinator.com/item?id=7994720).
- Well-implemented: Built upon the right things, including [Java NIO2 File API](https://docs.oracle.com/javase/8/docs/api/java/nio/file/package-summary.html) and [LiveData](https://developer.android.com/topic/libraries/architecture/livedata).
@@ -34,7 +32,7 @@ An open source Material Design file manager, for Android 5.0+.
Because I like Material Design, and clean Material Design.
-There are already a handful of powerful file managers, but most of them just isn't Material Design. And even among the ones with Material Design, they usually have various minor design flaws (layout, alignment, padding, icon, font, etc) across the app which makes me uncomfortable, while still being minor enough so that not everybody would care to fix it. So I had to create my own.
+There are already a handful of powerful file managers, but most of them just aren't Material Design. And even among the ones with Material Design, they usually have various minor design flaws (layout, alignment, padding, icon, font, etc) across the app which makes me uncomfortable, while still being minor enough so that not everybody would care to fix it. So I had to create my own.
Because I want an open source file manager.
diff --git a/README_zh-CN.md b/README_zh-CN.md
index ea86376c2..5220c1fbf 100644
--- a/README_zh-CN.md
+++ b/README_zh-CN.md
@@ -4,11 +4,9 @@
一个开源的 Material Design 文件管理器,适用于 Android 5.0+。
-[
](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) [
](https://f-droid.org/packages/me.zhanghai.android.files)
+[
](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) [
](https://f-droid.org/packages/me.zhanghai.android.files) [
](https://github.com/zhanghai/MaterialFiles/releases/latest/download/app-release-universal.apk)
-[在酷安上下载](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://learn.microsoft.com/en-us/globalization/reference/microsoft-language-resources)、[MIME 类型翻译](https://gitlab.freedesktop.org/xdg/shared-mime-info/-/tree/master/po))
## 预览
@@ -22,7 +20,7 @@
- 面包屑导航栏:点击导航栏所显示路径中的任一文件夹即可快速访问。
- Root 支持:使用 root 权限查看和管理文件。
- 压缩文件支持:查看、提取和创建常见的压缩文件。
-- NAS 支持:查看和管理 FTP、SFTP 和 SMB 服务器上的文件。
+- NAS 支持:查看和管理 FTP、SFTP、SMB 和 WebDAV 服务器上的文件。
- 主题:可定制的界面颜色,以及可选纯黑的夜间模式。
- Linux 友好:类似 [Nautilus](https://wiki.gnome.org/action/show/Apps/Files),支持符号链接、文件权限和 SELinux 上下文。
- 健壮性:使用 Linux 系统调用实现,而不是另一个 [`ls` 解析器](https://news.ycombinator.com/item?id=7994720)。
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.
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 7ec0af964..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(syscalls SHARED src/main/jni/syscalls.c)
-target_link_libraries(syscalls ${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})
diff --git a/app/build.gradle b/app/build.gradle
index 1c8cf9f59..62bc31790 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,10 +5,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-kapt'
-kapt {
- correctErrorTypes = true
-}
apply plugin: 'kotlin-parcelize'
apply from: '../signing.gradle'
@@ -20,8 +16,8 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.google.gms:google-services:4.3.10'
- classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
+ classpath 'com.google.gms:google-services:4.4.2'
+ classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2'
}
}
apply plugin: 'com.google.gms.google-services'
@@ -30,38 +26,53 @@ apply plugin: 'com.google.firebase.crashlytics'
android {
namespace 'me.zhanghai.android.files'
- compileSdkVersion 31
- ndkVersion '24.0.8215888'
- buildToolsVersion '32.0.0'
+ buildToolsVersion = '35.0.0'
+ compileSdk 35
+ ndkVersion '27.2.12479018'
defaultConfig {
applicationId 'me.zhanghai.android.files'
- minSdkVersion 21
- targetSdkVersion 31
- versionCode 31
- versionName '1.5.2'
+ minSdk 21
+ // Not supporting foreground service timeout yet.
+ //noinspection OldTargetApi
+ targetSdk 34
+ versionCode 39
+ versionName '1.7.4'
resValue 'string', 'app_version', versionName + ' (' + versionCode + ')'
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
+ buildConfig true
viewBinding true
}
+ androidResources {
+ generateLocaleConfig true
+ }
compileOptions {
+ coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
- freeCompilerArgs += [
- '-Xopt-in=kotlin.RequiresOptIn',
- ]
}
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
}
}
+ lint {
+ // For "Invalid package reference in library; not included in Android: javax.security.sasl.
+ // Referenced from org.apache.mina.proxy.ProxyAuthException."
+ warning 'InvalidPackage', 'MissingTranslation'
+ }
buildTypes {
release {
minifyEnabled true
@@ -76,14 +87,20 @@ android {
}
}
packagingOptions {
+ jniLibs {
+ useLegacyPackaging true
+ }
resources {
- excludes += ['META-INF/DEPENDENCIES']
+ excludes += [
+ 'META-INF/DEPENDENCIES',
+ 'org/bouncycastle/pqc/crypto/picnic/*'
+ ]
}
}
- lint {
- // For "Invalid package reference in library; not included in Android: javax.security.sasl.
- // Referenced from org.apache.mina.proxy.ProxyAuthException."
- warning 'InvalidPackage', 'MissingTranslation'
+ bundle {
+ language {
+ enableSplit = false
+ }
}
}
@@ -93,69 +110,71 @@ repositories {
}
}
dependencies {
+ implementation('com.github.bitfireAT:dav4jvm:c1bc143') {
+ exclude group: 'org.ogce', module: 'xpp3'
+ }
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.2.2'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.2")
+
// 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.9.0'
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.exifinterface:exifinterface:1.3.3'
- implementation 'androidx.fragment:fragment-ktx:1.4.1'
- def androidx_lifecycle_version = '2.4.1'
+ implementation 'androidx.activity:activity-ktx:1.9.3'
+ implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation 'androidx.core:core-ktx:1.15.0'
+ implementation 'androidx.drawerlayout:drawerlayout:1.2.0'
+ implementation 'androidx.exifinterface:exifinterface:1.3.7'
+ implementation 'androidx.fragment:fragment-ktx:1.8.5'
+ def androidx_lifecycle_version = '2.8.7'
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"
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.recyclerview:recyclerview:1.2.1'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
+ implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
- implementation 'com.google.android.material:material:1.6.0-rc01'
+ implementation 'com.google.android.material:material:1.12.0'
implementation 'com.caverock:androidsvg-aar:1.4'
- implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
+ implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.drakeet.drawer:drawer:1.0.3'
- debugImplementation 'com.facebook.stetho:stetho:1.6.0'
- // If added along with okhttp3, remove the related dontwarn from proguard-rules.pro.
- //debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
implementation 'com.h6ah4i.android.materialshadowninepatch:materialshadowninepatch:1.0.0'
implementation 'com.h6ah4i.android.widget.advrecyclerview:advrecyclerview:1.0.0'
+ // SMBJ 0.12.0 breaks anonymous authentication: https://github.com/hierynomus/smbj/issues/792
+ //noinspection GradleDependency
implementation ('com.hierynomus:smbj:0.11.5') {
// org.bouncycastle:bcprov-jdk15on uses bytecode version unsupported by Jetifier, so use
// org.bouncycastle:bcprov-jdk15to18 instead.
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
}
- implementation ('com.hierynomus:sshj:0.32.0') {
- // org.bouncycastle:bcprov-jdk15on uses bytecode version unsupported by Jetifier, so use
- // org.bouncycastle:bcprov-jdk15to18 instead.
- exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
+ def bouncy_castle_version = '1.78.1'
+ implementation "org.bouncycastle:bcprov-jdk15to18:$bouncy_castle_version"
+ implementation ('com.hierynomus:sshj:0.39.0') {
+ exclude group: 'org.bouncycastle', module: 'bcprov-jdk18on'
+ exclude group: 'org.bouncycastle', module: 'bcpkix-jdk18on'
}
- implementation 'com.jakewharton.threetenabp:threetenabp:1.4.0'
- // FloatingActionButtonSpeedDial 3.2.0 forces the user out of touch mode upon open and leaves a
- // strange focus effect on the first mini FAB.
- //noinspection GradleDependency
- implementation 'com.leinardi.android:speed-dial:3.1.1'
- implementation ('com.rapid7.client:dcerpc:0.12.0') {
- // org.bouncycastle:bcprov-jdk15on uses bytecode version unsupported by Jetifier, so use
- // org.bouncycastle:bcprov-jdk15to18 instead.
+ implementation "org.bouncycastle:bcpkix-jdk15to18:$bouncy_castle_version"
+ implementation 'com.leinardi.android:speed-dial:3.3.0'
+ implementation ('com.rapid7.client:dcerpc:0.12.1') {
+ // SMBJ-RPC depends on the JRE flavor of Guava which targets Java 8.
+ exclude group: 'com.google.guava', module: 'guava'
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
}
- implementation 'org.bouncycastle:bcprov-jdk15to18:1.70'
- // SMBJ-RPC depends on com.google.guava:guava:18.0, which conflicts with
- // com.google.guava:listenablefuture:1.0 pulled in by AndroidX Core
+ implementation 'com.google.guava:guava:33.3.1-android'
+ // Guava conflicts with 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.8.0'
+ implementation 'commons-net:commons-net:3.11.1'
// LicensesDialog 2.2.0 pulls in androidx.webkit and uses setForceDark() instead of correctly
// setting colors.
//noinspection GradleDependency
@@ -163,28 +182,22 @@ 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 'eu.agno3.jcifs:jcifs-ng:2.1.6'
- def coil_version = '1.4.0'
- implementation "io.coil-kt:coil:$coil_version"
- 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.fastscroll:library:1.1.8'
+ implementation 'dev.rikka.shizuku:api:13.1.5'
+ implementation ('eu.agno3.jcifs:jcifs-ng:2.1.10') {
+ exclude group: 'org.bouncycastle', module: 'bcprov-jdk18on'
+ }
+ implementation platform('io.coil-kt:coil-bom:2.7.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 '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.1.4'
implementation 'me.zhanghai.android.libselinux:library:2.1.0'
- implementation 'me.zhanghai.android.retrofile:library:1.1.1'
+ implementation 'me.zhanghai.android.retrofile:library:1.2.0'
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
@@ -194,12 +207,11 @@ 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.4.1'
- implementation platform('com.google.firebase:firebase-bom:29.0.3')
+ implementation platform('com.google.firebase:firebase-bom:33.5.1')
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-crashlytics-ndk'
//#endif
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 8cc37fa22..6c70e0117 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -33,18 +33,10 @@
-keepnames class * extends java.lang.Exception
# For Class.getEnumConstants()
-keepclassmembers enum * {
- public static **[] values();
+ public static **[] values();
}
-keepnames class me.zhanghai.android.files.** implements android.os.Parcelable
-# Apache Commons Compress
--dontwarn org.apache.commons.compress.compressors.**
--dontwarn org.apache.commons.compress.archivers.**
-# me.zhanghai.android.files.provider.archive.archiver.ArchiveWriter.sTarArchiveEntryLinkFlagsField
--keepclassmembers class org.apache.commons.compress.archivers.tar.TarArchiveEntry {
- byte linkFlag;
-}
-
# Apache FtpServer
-keepclassmembers class * implements org.apache.mina.core.service.IoProcessor {
public (java.util.concurrent.ExecutorService);
@@ -56,6 +48,10 @@
-keep class org.bouncycastle.jcajce.provider.** { *; }
-keep class org.bouncycastle.jce.provider.** { *; }
-# 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
+# SMBJ
+-dontwarn javax.el.**
+-dontwarn org.ietf.jgss.**
+-dontwarn sun.security.x509.X509Key
+
+# SMBJ-RPC
+-dontwarn java.rmi.UnmarshalException
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 31abbca20..4479e3e09 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,15 +9,19 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
@@ -25,7 +29,7 @@
+ android:maxSdkVersion="32" />
@@ -38,16 +42,19 @@
-->
+
@@ -76,8 +84,11 @@
+
+
+
@@ -89,26 +100,23 @@
-
+
-
-
+
-
-
+
- -->
-
+
@@ -170,6 +178,16 @@
android:label="@string/storage_edit_device_storage_title"
android:theme="@style/Theme.MaterialFiles.Translucent" />
+
+
+
+
+
+
+
+
@@ -267,6 +295,26 @@
android:autoRemoveFromRecents="true"
android:theme="@style/Theme.MaterialFiles.Translucent" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -298,9 +347,12 @@
-
+
-
+
+
+
+
+
+ * All Rights Reserved.
+ */
+
+package at.bitfire.dav4jvm;
+
+import java.io.IOException;
+
+import androidx.annotation.NonNull;
+import at.bitfire.dav4jvm.exception.DavException;
+import at.bitfire.dav4jvm.exception.HttpException;
+import kotlin.jvm.functions.Function0;
+import okhttp3.Response;
+
+public class DavResourceAccessor {
+ private DavResourceAccessor() {}
+
+ public static void checkStatus(@NonNull DavResource davResource, @NonNull Response response)
+ throws HttpException {
+ davResource.checkStatus(response);
+ }
+
+ public static Response followRedirects(@NonNull DavResource davResource,
+ @NonNull Function0 sendRequest) throws DavException, IOException {
+ return davResource.followRedirects$build(sendRequest);
+ }
+}
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/about/AboutFragment.kt b/app/src/main/java/me/zhanghai/android/files/about/AboutFragment.kt
index 9aadc7d1d..e93a4c988 100644
--- a/app/src/main/java/me/zhanghai/android/files/about/AboutFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/about/AboutFragment.kt
@@ -50,9 +50,6 @@ class AboutFragment : Fragment() {
binding.authorGitHubLayout.setOnClickListener {
startActivitySafe(AUTHOR_GITHUB_URI.createViewIntent())
}
- binding.authorGooglePlusLayout.setOnClickListener {
- startActivitySafe(AUTHOR_GOOGLE_PLUS_URI.createViewIntent())
- }
binding.authorTwitterLayout.setOnClickListener {
startActivitySafe(AUTHOR_TWITTER_URI.createViewIntent())
}
@@ -64,8 +61,6 @@ class AboutFragment : Fragment() {
Uri.parse("https://github.com/zhanghai/MaterialFiles/blob/master/PRIVACY.md")
private val AUTHOR_RESUME_URI = Uri.parse("https://resume.zhanghai.me/")
private val AUTHOR_GITHUB_URI = Uri.parse("https://github.com/zhanghai")
- private val AUTHOR_GOOGLE_PLUS_URI =
- Uri.parse("https://plus.google.com/100015937320889992498")
private val AUTHOR_TWITTER_URI = Uri.parse("https://twitter.com/zhanghai95")
}
}
diff --git a/app/src/main/java/me/zhanghai/android/files/app/AppInitializers.kt b/app/src/main/java/me/zhanghai/android/files/app/AppInitializers.kt
index 0cfcd0ed9..9f3f40c63 100644
--- a/app/src/main/java/me/zhanghai/android/files/app/AppInitializers.kt
+++ b/app/src/main/java/me/zhanghai/android/files/app/AppInitializers.kt
@@ -8,8 +8,6 @@ package me.zhanghai.android.files.app
import android.os.AsyncTask
import android.os.Build
import android.webkit.WebView
-import com.facebook.stetho.Stetho
-import com.jakewharton.threetenabp.AndroidThreeTen
import jcifs.context.SingletonContext
import me.zhanghai.android.files.BuildConfig
import me.zhanghai.android.files.coil.initializeCoil
@@ -22,18 +20,26 @@ import me.zhanghai.android.files.storage.FtpServerAuthenticator
import me.zhanghai.android.files.storage.SftpServerAuthenticator
import me.zhanghai.android.files.storage.SmbServerAuthenticator
import me.zhanghai.android.files.storage.StorageVolumeListLiveData
+import me.zhanghai.android.files.storage.WebDavServerAuthenticator
import me.zhanghai.android.files.theme.custom.CustomThemeHelper
import me.zhanghai.android.files.theme.night.NightModeHelper
import java.util.Properties
import me.zhanghai.android.files.provider.ftp.client.Client as FtpClient
import me.zhanghai.android.files.provider.sftp.client.Client as SftpClient
import me.zhanghai.android.files.provider.smb.client.Client as SmbClient
+import me.zhanghai.android.files.provider.webdav.client.Client as WebDavClient
val appInitializers = listOf(
- ::initializeCrashlytics, ::disableHiddenApiChecks, ::initializeThreeTen,
- ::initializeWebViewDebugging, ::initializeStetho, ::initializeCoil,
- ::initializeFileSystemProviders, ::upgradeApp, ::initializeLiveDataObjects,
- ::initializeCustomTheme, ::initializeNightMode, ::createNotificationChannels
+ ::initializeCrashlytics,
+ ::disableHiddenApiChecks,
+ ::initializeWebViewDebugging,
+ ::initializeCoil,
+ ::initializeFileSystemProviders,
+ ::upgradeApp,
+ ::initializeLiveDataObjects,
+ ::initializeCustomTheme,
+ ::initializeNightMode,
+ ::createNotificationChannels
)
private fun initializeCrashlytics() {
@@ -46,20 +52,12 @@ private fun disableHiddenApiChecks() {
HiddenApi.disableHiddenApiChecks()
}
-private fun initializeThreeTen() {
- AndroidThreeTen.init(application)
-}
-
private fun initializeWebViewDebugging() {
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true)
}
}
-private fun initializeStetho() {
- Stetho.initializeWithDefaults(application)
-}
-
private fun initializeFileSystemProviders() {
FileSystemProviders.install()
FileSystemProviders.overflowWatchEvents = true
@@ -75,6 +73,7 @@ private fun initializeFileSystemProviders() {
FtpClient.authenticator = FtpServerAuthenticator
SftpClient.authenticator = SftpServerAuthenticator
SmbClient.authenticator = SmbServerAuthenticator
+ WebDavClient.authenticator = WebDavServerAuthenticator
}
private fun initializeLiveDataObjects() {
diff --git a/app/src/main/java/me/zhanghai/android/files/app/AppUpgrader.kt b/app/src/main/java/me/zhanghai/android/files/app/AppUpgrader.kt
index 9879f2669..5faea314b 100644
--- a/app/src/main/java/me/zhanghai/android/files/app/AppUpgrader.kt
+++ b/app/src/main/java/me/zhanghai/android/files/app/AppUpgrader.kt
@@ -16,6 +16,8 @@ private const val VERSION_CODE_1_2_0 = 22
private const val VERSION_CODE_1_3_0 = 24
private const val VERSION_CODE_1_4_0 = 26
private const val VERSION_CODE_1_5_0 = 29
+private const val VERSION_CODE_1_6_0 = 32
+private const val VERSION_CODE_1_7_2 = 37
private const val VERSION_CODE_LATEST = BuildConfig.VERSION_CODE
private var lastVersionCode: Int
@@ -52,5 +54,11 @@ private fun upgradeAppFrom(lastVersionCode: Int) {
if (lastVersionCode < VERSION_CODE_1_5_0) {
upgradeAppTo1_5_0()
}
+ if (lastVersionCode < VERSION_CODE_1_6_0) {
+ upgradeAppTo1_6_0()
+ }
+ if (lastVersionCode < VERSION_CODE_1_7_2) {
+ upgradeAppTo1_7_2()
+ }
// Continue with new `if`s on lastVersionCode instead of `else if`.
}
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 25d76dbea..b6b24c7fd 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
@@ -7,6 +7,7 @@ package me.zhanghai.android.files.app
import android.content.SharedPreferences
import android.net.Uri
+import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.StringRes
@@ -18,6 +19,7 @@ import me.zhanghai.android.files.compat.readBooleanCompat
import me.zhanghai.android.files.compat.writeBooleanCompat
import me.zhanghai.android.files.compat.writeParcelableListCompat
import me.zhanghai.android.files.file.DocumentTreeUri
+import me.zhanghai.android.files.file.asExternalStorageUriOrNull
import me.zhanghai.android.files.file.displayName
import me.zhanghai.android.files.file.storageVolume
import me.zhanghai.android.files.filelist.FileSortOptions
@@ -28,6 +30,7 @@ import me.zhanghai.android.files.provider.common.ByteString
import me.zhanghai.android.files.provider.common.moveToByteString
import me.zhanghai.android.files.provider.content.ContentFileSystem
import me.zhanghai.android.files.provider.document.DocumentFileSystem
+import me.zhanghai.android.files.provider.document.resolver.ExternalStorageProviderHacks
import me.zhanghai.android.files.provider.linux.LinuxFileSystem
import me.zhanghai.android.files.provider.root.RootStrategy
import me.zhanghai.android.files.provider.sftp.SftpFileSystem
@@ -35,6 +38,7 @@ import me.zhanghai.android.files.provider.smb.SmbFileSystem
import me.zhanghai.android.files.storage.DocumentTree
import me.zhanghai.android.files.storage.FileSystemRoot
import me.zhanghai.android.files.storage.PrimaryStorageVolume
+import me.zhanghai.android.files.util.StableUriParceler
import me.zhanghai.android.files.util.asBase64
import me.zhanghai.android.files.util.readParcelable
import me.zhanghai.android.files.util.readParcelableListCompat
@@ -94,8 +98,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()
@@ -520,3 +524,109 @@ private fun migrateSftpServersSetting1_5_0() {
}
defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
}
+
+internal fun upgradeAppTo1_6_0() {
+ addViewTypePathSetting1_6_0()
+}
+
+private fun addViewTypePathSetting1_6_0() {
+ val keys = pathSharedPreferences.all.keys.toSet()
+ val sortOptionsKey = application.getString(R.string.pref_key_file_list_sort_options)
+ val viewTypeKey = application.getString(R.string.pref_key_file_list_view_type)
+ val defaultViewType = application.getString(R.string.pref_default_value_file_list_view_type)
+ for (key in keys) {
+ if (!key.startsWith(sortOptionsKey)) {
+ continue
+ }
+ val newKey = key.replaceFirst(sortOptionsKey, viewTypeKey)
+ if (newKey in keys) {
+ continue
+ }
+ pathSharedPreferences.edit { putString(newKey, defaultViewType) }
+ }
+}
+
+internal fun upgradeAppTo1_7_2() {
+ migrateDocumentManagerShortcutSetting1_7_2()
+}
+
+private fun migrateDocumentManagerShortcutSetting1_7_2() {
+ val key = application.getString(R.string.pref_key_storages)
+ val oldBytes =
+ defaultSharedPreferences.getString(key, null)?.asBase64()?.toByteArray() ?: return
+ val newBytes =
+ try {
+ Parcel.obtain().use { newParcel ->
+ Parcel.obtain().use { oldParcel ->
+ oldParcel.unmarshall(oldBytes, 0, oldBytes.size)
+ oldParcel.setDataPosition(0)
+ newParcel.writeInt(oldParcel.readInt())
+ readWriteLengthPrefixedValue(oldParcel, newParcel) {
+ val size = oldParcel.readInt()
+ newParcel.writeInt(size)
+ repeat(size) {
+ val oldPosition = oldParcel.dataPosition()
+ oldParcel.readInt()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // Skip prefix length.
+ oldParcel.readInt()
+ }
+ val className = oldParcel.readString()
+ oldParcel.setDataPosition(oldPosition)
+ when (className) {
+ "me.zhanghai.android.files.storage.DocumentManagerShortcut" -> {
+ newParcel.writeInt(oldParcel.readInt())
+ readWriteLengthPrefixedValue(oldParcel, newParcel) {
+ oldParcel.readString()
+ newParcel.writeString(
+ "me.zhanghai.android.files.storage" +
+ ".ExternalStorageShortcut"
+ )
+ val id = oldParcel.readLong()
+ newParcel.writeLong(id)
+ val customName = oldParcel.readString()
+ newParcel.writeString(customName)
+ var uri = StableUriParceler.create(oldParcel)!!
+ if (uri.asExternalStorageUriOrNull() == null) {
+ // Reset to a valid external storage URI.
+ uri =
+ ExternalStorageProviderHacks
+ .DOCUMENT_URI_ANDROID_DATA
+ }
+ with(StableUriParceler) { uri.write(newParcel, 0) }
+ }
+ }
+ else -> {
+ val storage = oldParcel.readValue(appClassLoader)
+ newParcel.writeValue(storage)
+ }
+ }
+ }
+ }
+ }
+ newParcel.marshall()
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
+}
+
+private fun readWriteLengthPrefixedValue(oldParcel: Parcel, newParcel: Parcel, block: () -> Unit) {
+ var lengthPosition = 0
+ var startPosition = 0
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ oldParcel.readInt()
+ lengthPosition = newParcel.dataPosition()
+ newParcel.writeInt(-1)
+ startPosition = newParcel.dataPosition()
+ }
+ block()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val endPosition = newParcel.dataPosition()
+ newParcel.setDataPosition(lengthPosition)
+ newParcel.writeInt(endPosition - startPosition)
+ newParcel.setDataPosition(endPosition)
+ }
+}
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/app/SystemServices.kt b/app/src/main/java/me/zhanghai/android/files/app/SystemServices.kt
index 3b1818c56..388fdbbb1 100644
--- a/app/src/main/java/me/zhanghai/android/files/app/SystemServices.kt
+++ b/app/src/main/java/me/zhanghai/android/files/app/SystemServices.kt
@@ -17,32 +17,35 @@ import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import me.zhanghai.android.files.compat.getSystemServiceCompat
import me.zhanghai.android.files.compat.mainExecutorCompat
+import okhttp3.OkHttpClient
import java.util.concurrent.Executor
val appClassLoader = AppProvider::class.java.classLoader
+val clipboardManager: ClipboardManager by lazy {
+ application.getSystemServiceCompat(ClipboardManager::class.java)
+}
+
val contentResolver: ContentResolver by lazy { application.contentResolver }
val defaultSharedPreferences: SharedPreferences by lazy {
PreferenceManager.getDefaultSharedPreferences(application)
}
-val mainExecutor: Executor by lazy { application.mainExecutorCompat }
-
-val packageManager: PackageManager by lazy { application.packageManager }
-
-val clipboardManager: ClipboardManager by lazy {
- application.getSystemServiceCompat(ClipboardManager::class.java)
-}
+val okHttpClient: OkHttpClient by lazy { OkHttpClient() }
val inputMethodManager: InputMethodManager by lazy {
application.getSystemServiceCompat(InputMethodManager::class.java)
}
+val mainExecutor: Executor by lazy { application.mainExecutorCompat }
+
val notificationManager: NotificationManagerCompat by lazy {
NotificationManagerCompat.from(application)
}
+val packageManager: PackageManager by lazy { application.packageManager }
+
val powerManager: PowerManager by lazy {
application.getSystemServiceCompat(PowerManager::class.java)
}
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..4617f40e3
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/coil/CoilUtils.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022 Hai Zhang
+ * All Rights Reserved.
+ */
+
+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.filelist.isRemotePath
+
+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() = if (isRemotePath) DataSource.NETWORK else DataSource.DISK
+
+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..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
@@ -10,16 +10,15 @@ 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.decode.DataSource
-import coil.decode.Options
+import coil.ImageLoader
+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
@@ -32,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
@@ -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
@@ -53,75 +54,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,24 +91,24 @@ class PathAttributesFetcher(
}
if (thumbnail != null) {
return DrawableResult(
- thumbnail.toDrawable(context.resources), true, DataSource.DISK
+ thumbnail.toDrawable(options.context.resources), true, path.dataSource
)
}
}
- 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()
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,12 +116,11 @@ 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, path.dataSource
)
}
- mimeType.isMedia && (path.isLinuxPath || path.isDocumentPath) -> {
+ mimeType.isMedia && path.isMediaMetadataRetrieverCompatible -> {
val embeddedPicture = try {
MediaMetadataRetriever().use { retriever ->
retriever.setDataSource(path)
@@ -172,12 +132,14 @@ class PathAttributesFetcher(
}
if (embeddedPicture != null) {
return SourceResult(
- embeddedPicture.inputStream().source().buffer(), null, DataSource.DISK
+ ImageSource(
+ embeddedPicture.inputStream().source().buffer(), options.context
+ ), null, path.dataSource
)
}
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 +147,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..d18005b03
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/coil/VideoFrameFetcher.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.Options
+import coil.request.videoFrameOption
+import coil.request.videoFramePercent
+import me.zhanghai.android.files.compat.getFrameAtTimeCompat
+import me.zhanghai.android.files.compat.getScaledFrameAtTimeCompat
+import me.zhanghai.android.files.compat.use
+import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
+import kotlin.math.roundToLong
+
+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 percentage tried by totem-video-thumbnailer.
+ // @see https://gitlab.gnome.org/GNOME/totem/-/blob/master/src/totem-video-thumbnailer.c#L543
+ val framePercent = options.parameters.videoFramePercent() ?: (1.0 / 3.0)
+ val frameMicros = TimeUnit.MICROSECONDS.convert(
+ (framePercent * durationMillis).roundToLong(), TimeUnit.MILLISECONDS
+ )
+ 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 }
+ 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.getScaledFrameAtTimeCompat(
+ frameMicros, frameOption, width, height, bitmapParams
+ )
+ } else {
+ retriever.getFrameAtTimeCompat(frameMicros, frameOption, bitmapParams)?.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 != true || 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
+ )
+ }
+
+ 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/compat/ActivityCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/ActivityCompat.kt
index c9c2136eb..24dd791db 100644
--- a/app/src/main/java/me/zhanghai/android/files/compat/ActivityCompat.kt
+++ b/app/src/main/java/me/zhanghai/android/files/compat/ActivityCompat.kt
@@ -11,7 +11,6 @@ import android.graphics.Color
import android.os.Build
import androidx.annotation.StyleRes
import androidx.core.app.ActivityCompat
-import me.zhanghai.android.files.R
import me.zhanghai.android.files.util.getColorByAttr
fun Activity.recreateCompat() {
@@ -21,7 +20,7 @@ fun Activity.recreateCompat() {
fun Activity.setThemeCompat(@StyleRes resid: Int) {
setTheme(resid)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- val surfaceColor = getColorByAttr(R.attr.colorSurface)
+ val surfaceColor = getColorByAttr(com.google.android.material.R.attr.colorSurface)
if (surfaceColor != 0 && Color.alpha(surfaceColor) == 0xFF) {
@Suppress("DEPRECATION")
setTaskDescription(TaskDescription(null, null, surfaceColor))
diff --git a/app/src/main/java/me/zhanghai/android/files/compat/ContextCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/ContextCompat.kt
index 5b686320d..49f696e7f 100644
--- a/app/src/main/java/me/zhanghai/android/files/compat/ContextCompat.kt
+++ b/app/src/main/java/me/zhanghai/android/files/compat/ContextCompat.kt
@@ -6,7 +6,10 @@
package me.zhanghai.android.files.compat
import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.util.AttributeSet
@@ -38,6 +41,12 @@ fun Context.getColorStateListCompat(@ColorRes id: Int): ColorStateList =
fun Context.getDrawableCompat(@DrawableRes id: Int): Drawable =
AppCompatResources.getDrawable(this, id)!!
+fun Context.getSystemServiceCompat(serviceClass: Class): T =
+ ContextCompat.getSystemService(this, serviceClass)!!
+
+val Context.mainExecutorCompat: Executor
+ get() = ContextCompat.getMainExecutor(this)
+
@SuppressLint("RestrictedApi")
fun Context.obtainStyledAttributesCompat(
set: AttributeSet? = null,
@@ -60,11 +69,11 @@ inline fun TintTypedArray.use(block: (TintTypedArray) -> R): R {
}
}
-val Context.mainExecutorCompat: Executor
- get() = ContextCompat.getMainExecutor(this)
-
-fun Context.getSystemServiceCompat(serviceClass: Class): T =
- ContextCompat.getSystemService(this, serviceClass)!!
+fun Context.registerReceiverCompat(
+ receiver: BroadcastReceiver?,
+ filter: IntentFilter,
+ flags: Int
+): Intent? = ContextCompat.registerReceiver(this, receiver, filter, flags)
@RestrictedHiddenApi
private val getThemeResIdMethod by lazyReflectedMethod(Context::class.java, "getThemeResId")
diff --git a/app/src/main/java/me/zhanghai/android/files/compat/DateTimeCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/DateTimeCompat.kt
deleted file mode 100644
index 6a39604ab..000000000
--- a/app/src/main/java/me/zhanghai/android/files/compat/DateTimeCompat.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package me.zhanghai.android.files.compat
-
-import org.threeten.bp.DateTimeUtils
-import org.threeten.bp.Instant
-import java.util.Calendar
-
-fun Calendar.toInstantCompat(): Instant = DateTimeUtils.toInstant(this)
diff --git a/app/src/main/java/me/zhanghai/android/files/compat/DocumentsContractCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/DocumentsContractCompat.kt
index 0cf1e3736..c5e6d4d13 100644
--- a/app/src/main/java/me/zhanghai/android/files/compat/DocumentsContractCompat.kt
+++ b/app/src/main/java/me/zhanghai/android/files/compat/DocumentsContractCompat.kt
@@ -9,6 +9,7 @@ import android.content.ContentResolver
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
+import me.zhanghai.android.files.app.packageManager
object DocumentsContractCompat {
const val EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI"
@@ -21,6 +22,17 @@ object DocumentsContractCompat {
private const val PATH_CHILDREN = "children"
private const val PATH_TREE = "tree"
+ /** @see DocumentsContract.PACKAGE_DOCUMENTS_UI */
+ fun getDocumentsUiPackage(): String? {
+ // See android.permission.cts.ProviderPermissionTest.testManageDocuments()
+ val packageInfos = packageManager.getPackagesHoldingPermissions(
+ arrayOf(android.Manifest.permission.MANAGE_DOCUMENTS), 0
+ )
+ val packageInfo = packageInfos.firstOrNull { it.packageName.endsWith(".documentsui") }
+ ?: packageInfos.firstOrNull()
+ return packageInfo?.packageName
+ }
+
/** @see DocumentsContract.isDocumentUri */
fun isDocumentUri(uri: Uri): Boolean {
if (uri.scheme != ContentResolver.SCHEME_CONTENT) {
diff --git a/app/src/main/java/me/zhanghai/android/files/compat/InputStreamCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/InputStreamCompat.kt
new file mode 100644
index 000000000..445e9187b
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/compat/InputStreamCompat.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2024 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.compat
+
+import java.io.IOException
+import java.io.InputStream
+import kotlin.reflect.KClass
+
+fun KClass.nullInputStream(): InputStream =
+ object : InputStream() {
+ private var closed = false
+
+ override fun read(): Int {
+ ensureOpen()
+ return -1
+ }
+
+ override fun read(bytes: ByteArray, offset: Int, length: Int): Int {
+ if (!(offset >= 0 && length >= 0 && length <= bytes.size - offset)) {
+ throw IndexOutOfBoundsException()
+ }
+ ensureOpen()
+ return if (length == 0) 0 else -1
+ }
+
+ override fun skip(length: Long): Long {
+ ensureOpen()
+ return 0
+ }
+
+ override fun available(): Int {
+ ensureOpen()
+ return 0
+ }
+
+ override fun close() {
+ closed = true
+ }
+
+ private fun ensureOpen() {
+ if (closed) {
+ throw IOException("Stream closed")
+ }
+ }
+ }
diff --git a/app/src/main/java/me/zhanghai/android/files/compat/KotlinCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/KotlinCompat.kt
index 459dfa354..cadc64136 100644
--- a/app/src/main/java/me/zhanghai/android/files/compat/KotlinCompat.kt
+++ b/app/src/main/java/me/zhanghai/android/files/compat/KotlinCompat.kt
@@ -5,30 +5,12 @@
package me.zhanghai.android.files.compat
-import java.io.Closeable
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
import kotlin.comparisons.reversed as kotlinReversed
-import kotlin.io.use as kotlinUse
-import kotlin.use as kotlinUse
+import kotlin.collections.removeFirst as kotlinRemoveFirst
+import kotlin.collections.removeLast as kotlinRemoveLast
-// @see https://youtrack.jetbrains.com/issue/KT-35216
-@OptIn(ExperimentalContracts::class)
-inline fun T.use(block: (T) -> R): R {
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- return kotlinUse(block)
-}
+fun Comparator.reversedCompat(): Comparator = kotlinReversed()
-// @see https://youtrack.jetbrains.com/issue/KT-35216
-@OptIn(ExperimentalContracts::class)
-inline fun T.use(block: (T) -> R): R {
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
- return kotlinUse(block)
-}
+fun MutableList.removeFirstCompat(): T = kotlinRemoveFirst()
-fun Comparator.reversedCompat(): Comparator = kotlinReversed()
+fun MutableList.removeLastCompat(): T = kotlinRemoveLast()
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
new file mode 100644
index 000000000..ed98883f6
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/compat/LocaleConfigCompat.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.compat
+
+import android.app.LocaleConfig
+import android.content.Context
+import android.content.res.XmlResourceParser
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.annotation.XmlRes
+import androidx.core.content.res.ResourcesCompat
+import androidx.core.os.LocaleListCompat
+import org.xmlpull.v1.XmlPullParser
+import java.io.FileNotFoundException
+
+/**
+ * @see android.app.LocaleConfig
+ */
+class LocaleConfigCompat(context: Context) {
+ var status = 0
+ private set
+
+ var supportedLocales: LocaleListCompat? = null
+ private set
+
+ init {
+ val impl = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Api33Impl(context)
+ } else {
+ Api21Impl(context)
+ }
+ status = impl.status
+ supportedLocales = impl.supportedLocales
+ }
+
+ companion object {
+ /**
+ * Succeeded reading the LocaleConfig structure stored in an XML file.
+ */
+ const val STATUS_SUCCESS = 0
+
+ /**
+ * No android:localeConfig tag on .
+ */
+ const val STATUS_NOT_SPECIFIED = 1
+
+ /**
+ * Malformed input in the XML file where the LocaleConfig was stored.
+ */
+ const val STATUS_PARSING_FAILED = 2
+ }
+
+ private abstract class Impl {
+ abstract val status: Int
+ abstract val supportedLocales: LocaleListCompat?
+ }
+
+ private class Api21Impl(context: Context) : Impl() {
+ override var status = 0
+ private set
+
+ override var supportedLocales: LocaleListCompat? = null
+ private set
+
+ init {
+ val resourceId = try {
+ getLocaleConfigResourceId(context)
+ } catch (e: Exception) {
+ Log.w(TAG, "The resource file pointed to by the given resource ID isn't found.", e)
+ }
+ if (resourceId == ResourcesCompat.ID_NULL) {
+ status = STATUS_NOT_SPECIFIED
+ } else {
+ val resources = context.resources
+ try {
+ supportedLocales = resources.getXml(resourceId).use { parseLocaleConfig(it) }
+ status = STATUS_SUCCESS
+ } catch (e: Exception) {
+ val resourceEntryName = resources.getResourceEntryName(resourceId)
+ Log.w(TAG, "Failed to parse XML configuration from $resourceEntryName", e)
+ status = STATUS_PARSING_FAILED
+ }
+ }
+ }
+
+ // @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) {
+ if (!isAndroidManifestFound) {
+ ++cookie
+ continue
+ } else {
+ break
+ }
+ }
+ isAndroidManifestFound = true
+ parser.use {
+ do {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+ if (parser.name != TAG_MANIFEST) {
+ parser.skipCurrentTag()
+ continue
+ }
+ if (parser.getAttributeValue(null, ATTR_PACKAGE) != context.packageName) {
+ break
+ }
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+ if (parser.name != TAG_APPLICATION) {
+ parser.skipCurrentTag()
+ continue
+ }
+ return parser.getAttributeResourceValue(
+ NAMESPACE_ANDROID, ATTR_LOCALE_CONFIG, ResourcesCompat.ID_NULL
+ )
+ }
+ } while (parser.next() != XmlPullParser.END_DOCUMENT)
+ }
+ ++cookie
+ }
+ return ResourcesCompat.ID_NULL
+ }
+
+ private fun parseLocaleConfig(parser: XmlResourceParser): LocaleListCompat {
+ val localeNames = mutableSetOf()
+ do {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+ if (parser.name != TAG_LOCALE_CONFIG) {
+ parser.skipCurrentTag()
+ continue
+ }
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.eventType != XmlPullParser.START_TAG) {
+ continue
+ }
+ if (parser.name != TAG_LOCALE) {
+ parser.skipCurrentTag()
+ continue
+ }
+ localeNames += parser.getAttributeValue(NAMESPACE_ANDROID, ATTR_NAME)
+ parser.skipCurrentTag()
+ }
+ } while (parser.next() != XmlPullParser.END_DOCUMENT)
+ return LocaleListCompat.forLanguageTags(localeNames.joinToString(","))
+ }
+
+ private fun XmlPullParser.skipCurrentTag() {
+ val outerDepth = depth
+ var type: Int
+ do {
+ type = next()
+ } while (type != XmlPullParser.END_DOCUMENT &&
+ (type != XmlPullParser.END_TAG || depth > outerDepth))
+ }
+
+ companion object {
+ private const val TAG = "LocaleConfigCompat"
+
+ private const val FILE_NAME_ANDROID_MANIFEST = "AndroidManifest.xml"
+
+ private const val TAG_APPLICATION = "application"
+ private const val TAG_LOCALE_CONFIG = "locale-config"
+ private const val TAG_LOCALE = "locale"
+ private const val TAG_MANIFEST = "manifest"
+
+ private const val NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
+
+ private const val ATTR_LOCALE_CONFIG = "localeConfig"
+ private const val ATTR_NAME = "name"
+ private const val ATTR_PACKAGE = "package"
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private class Api33Impl(context: Context) : Impl() {
+ override var status: Int = 0
+ private set
+
+ override var supportedLocales: LocaleListCompat? = null
+ private set
+
+ init {
+ val platformLocaleConfig = LocaleConfig(context)
+ status = platformLocaleConfig.status
+ supportedLocales = platformLocaleConfig.supportedLocales
+ ?.let { LocaleListCompat.wrap(it) }
+ }
+ }
+}
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 {
diff --git a/app/src/main/java/me/zhanghai/android/files/compat/MenuCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/MenuCompat.kt
new file mode 100644
index 000000000..2e7846ca6
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/compat/MenuCompat.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.compat
+
+import android.view.Menu
+import androidx.core.view.MenuCompat
+
+fun Menu.setGroupDividerEnabledCompat(groupDividerEnabled: Boolean) {
+ MenuCompat.setGroupDividerEnabled(this, groupDividerEnabled)
+}
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..a351c0ff8 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
@@ -10,141 +10,353 @@ import android.webkit.MimeTypeMap
// Generated by mime/generate-extensions.sh and mime/generate-code.sh .
private val extensionToMimeTypeMap = mapOf(
"%" to "application/x-trash",
- "323" to "text/h323",
+ "123" to "application/vnd.lotus-1-2-3",
+ "1905.1" to "application/vnd.ieee.1905",
+ "1clr" to "application/clr",
+ "1km" to "application/vnd.1000minds.decision-model+xml",
+ "210" to "application/p21",
+ "3dm" to "text/vnd.in3d.3dml",
+ "3dml" to "text/vnd.in3d.3dml",
"3g2" to "video/3gpp2",
"3ga" to "audio/3gpp",
"3gp" to "video/3gpp",
"3gp2" to "video/3gpp2",
"3gpp" to "video/3gpp",
"3gpp2" to "video/3gpp2",
+ "3mf" to "application/vnd.ms-3mfdocument",
+ "3tz" to "application/vnd.maxar.archive.3tz+zip",
+ "726" to "audio/32kadpcm",
"7z" to "application/x-7z-compressed",
+ "AMR" to "audio/AMR",
+ "AWB" to "audio/AMR-WB",
+ "CQL" to "text/cql",
+ "ELN" to "application/vnd.eln+zip",
+ "J2C" to "image/j2c",
+ "J2K" to "image/j2c",
+ "PGB" to "image/vnd.globalgraphics.pgb",
+ "QCP" to "audio/EVRC-QCP",
+ "SAR" to "application/vnd.sar",
+ "VES" to "application/vnd.ves.encrypted",
+ "VFK" to "text/vnd.exchangeable",
+ "a" to "text/vnd.a",
+ "a2l" to "application/A2L",
"a52" to "audio/ac3",
+ "aa3" to "audio/ATRAC3",
"aac" to "audio/aac",
+ "aal" to "audio/ATRAC-ADVANCED-LOSSLESS",
+ "abc" to "text/vnd.abc",
"abw" to "application/x-abiword",
+ "ac" to "application/pkix-attr-cert",
+ "ac2" to "application/vnd.banana-accounting",
"ac3" to "audio/ac3",
+ "acc" to "application/vnd.americandynamics.acc",
+ "acn" to "audio/asc",
+ "acu" to "application/vnd.acucobol",
+ "acutc" to "application/vnd.acucorp",
"adt" to "audio/aac",
"adts" to "audio/aac",
+ "aep" to "application/vnd.audiograph",
+ "afp" to "application/vnd.afpc.modca",
+ "age" to "application/vnd.age",
+ "ahead" to "application/vnd.ahead.space",
+ "ahv" to "application/vnd.android.haptics.vibration+xml",
"ai" to "application/postscript",
"aif" to "audio/x-aiff",
"aifc" to "audio/x-aiff",
"aiff" to "audio/x-aiff",
+ "aion" to "application/vnd.veritone.aion+json",
+ "ait" to "application/vnd.dvb.ait",
"alc" to "chemical/x-alchemy",
+ "ami" to "application/vnd.amiga.ami",
+ "aml" to "application/AML",
+ "amlx" to "application/automationml-amlx+zip",
"amr" to "audio/amr",
"anx" to "application/annodex",
+ "apex" to "application/vnd.apexlang",
+ "apexlang" to "application/vnd.apexlang",
"apk" to "application/vnd.android.package-archive",
+ "apkg" to "application/vnd.anki",
+ "apng" to "image/apng",
"appcache" to "text/cache-manifest",
- "application" to "application/x-ms-application",
+ "apr" to "application/vnd.lotus-approach",
+ "apxml" to "application/auth-policy+xml",
+ "arrow" to "application/vnd.apache.arrow.file",
+ "arrows" to "application/vnd.apache.arrow.stream",
"art" to "image/x-jg",
+ "artisan" to "application/vnd.artisan+json",
"arw" to "image/x-sony-arw",
- "asc" to "text/plain",
- "asf" to "video/x-ms-asf",
+ "asc" to "application/pgp-keys",
+ "ascii" to "text/vnd.ascii-art",
+ "asf" to "application/vnd.ms-asf",
+ "asice" to "application/vnd.etsi.asic-e+zip",
+ "asics" to "application/vnd.etsi.asic-s+zip",
"asn" to "chemical/x-ncbi-asn1-spec",
"aso" to "chemical/x-ncbi-asn1-binary",
- "asx" to "video/x-ms-asf",
+ "ass" to "audio/aac",
+ "at3" to "audio/ATRAC3",
+ "atc" to "application/vnd.acucorp",
+ "atf" to "application/ATF",
+ "atfx" to "application/ATFX",
"atom" to "application/atom+xml",
"atomcat" to "application/atomcat+xml",
+ "atomdeleted" to "application/atomdeleted+xml",
"atomsrv" to "application/atomserv+xml",
+ "atomsvc" to "application/atomsvc+xml",
+ "atx" to "audio/ATRAC-X",
+ "atxml" to "application/ATXML",
"au" to "audio/basic",
+ "auc" to "application/tamp-apex-update-confirm",
+ "avci" to "image/avci",
+ "avcs" to "image/avcs",
"avi" to "video/avi",
- "awb" to "audio/amr-wb",
+ "avif" to "image/avif",
+ "awb" to "audio/AMR-WB",
"axa" to "audio/annodex",
"axv" to "video/annodex",
+ "azf" to "application/vnd.airzip.filesecure.azf",
+ "azs" to "application/vnd.airzip.filesecure.azs",
+ "azv" to "image/vnd.airzip.accelerator.azv",
+ "azw3" to "application/vnd.amazon.mobi8-ebook",
"b" to "chemical/x-molconn-Z",
+ "b16" to "image/vnd.pco.b16",
"bak" to "application/x-trash",
+ "bar" to "application/vnd.qualcomm.brew-app-res",
+ "bary" to "model/vnd.bary",
"bat" to "application/x-msdos-program",
"bcpio" to "application/x-bcpio",
+ "bdm" to "application/vnd.syncml.dm+wbxml",
+ "bed" to "application/vnd.realvnc.bed",
+ "bh2" to "application/vnd.fujitsu.oasysprs",
"bib" to "text/x-bibtex",
+ "bik" to "video/vnd.radgamettools.bink",
"bin" to "application/octet-stream",
+ "bk2" to "video/vnd.radgamettools.bink",
+ "bkm" to "application/vnd.nervana",
+ "bmed" to "multipart/vnd.bint.med-plus",
+ "bmi" to "application/vnd.bmi",
+ "bmml" to "application/vnd.balsamiq.bmml+xml",
"bmp" to "image/x-ms-bmp",
+ "bmpr" to "application/vnd.balsamiq.bmpr",
"boo" to "text/x-boo",
"book" to "application/x-maker",
+ "box" to "application/vnd.previewsystems.box",
+ "bpd" to "application/vnd.hbci",
"brf" to "text/plain",
"bsd" to "chemical/x-crossfire",
+ "bsp" to "model/vnd.valve.source.compiled-map",
+ "btf" to "image/prs.btif",
+ "btif" to "image/prs.btif",
"c" to "text/x-csrc",
"c++" to "text/x-c++src",
+ "c11amc" to "application/vnd.cluetrust.cartomobile-config",
+ "c11amz" to "application/vnd.cluetrust.cartomobile-config-pkg",
"c3d" to "chemical/x-chem3d",
- "cab" to "application/x-cab",
+ "c3ex" to "application/cccex",
+ "c4d" to "application/vnd.clonk.c4group",
+ "c4f" to "application/vnd.clonk.c4group",
+ "c4g" to "application/vnd.clonk.c4group",
+ "c4p" to "application/vnd.clonk.c4group",
+ "c4u" to "application/vnd.clonk.c4group",
+ "c9r" to "application/vnd.cryptomator.encrypted",
+ "c9s" to "application/vnd.cryptomator.encrypted",
+ "cab" to "application/vnd.ms-cab-compressed",
"cac" to "chemical/x-cache",
"cache" to "chemical/x-cache",
"cap" to "application/vnd.tcpdump.pcap",
+ "car" to "application/vnd.ipld.car",
+ "carjson" to "application/vnd.eu.kasparian.car+json",
"cascii" to "chemical/x-cactvs-binary",
"cat" to "application/vnd.ms-pki.seccat",
"cbin" to "chemical/x-cactvs-binary",
- "cbr" to "application/x-cbr",
- "cbz" to "application/x-cbz",
+ "cbor" to "application/cbor",
+ "cbr" to "application/vnd.comicbook-rar",
+ "cbz" to "application/vnd.comicbook+zip",
"cc" to "text/x-c++src",
+ "ccc" to "text/vnd.net2phone.commcenter.command",
+ "ccmp" to "application/ccmp+xml",
+ "ccxml" to "application/ccxml+xml",
"cda" to "application/x-cdf",
+ "cdbcmsg" to "application/vnd.contact.cmsg",
"cdf" to "application/x-cdf",
+ "cdfx" to "application/CDFX+XML",
+ "cdkey" to "application/vnd.mediastation.cdkey",
+ "cdmia" to "application/cdmi-capability",
+ "cdmic" to "application/cdmi-container",
+ "cdmid" to "application/cdmi-domain",
+ "cdmio" to "application/cdmi-object",
+ "cdmiq" to "application/cdmi-queue",
"cdr" to "image/x-coreldraw",
"cdt" to "image/x-coreldrawtemplate",
"cdx" to "chemical/x-cdx",
+ "cdxml" to "application/vnd.chemdraw+xml",
"cdy" to "application/vnd.cinderella",
+ "cea" to "application/CEA",
"cef" to "chemical/x-cxf",
+ "cellml" to "application/cellml+xml",
"cer" to "application/pkix-cert",
+ "cgm" to "image/cgm",
"chm" to "chemical/x-chemdraw",
- "chrt" to "application/x-kchart",
+ "chrt" to "application/vnd.kde.kchart",
"cif" to "chemical/x-cif",
+ "cii" to "application/vnd.anser-web-certificate-issue-initiation",
+ "cil" to "application/vnd.ms-artgalry",
+ "cl" to "application/simple-filter+xml",
+ "cla" to "application/vnd.claymore",
"class" to "application/java-vm",
+ "cld" to "model/vnd.cld",
+ "clkk" to "application/vnd.crick.clicker.keyboard",
+ "clkp" to "application/vnd.crick.clicker.palette",
+ "clkt" to "application/vnd.crick.clicker.template",
+ "clkw" to "application/vnd.crick.clicker.wordbank",
+ "clkx" to "application/vnd.crick.clicker",
"cls" to "text/x-tex",
+ "clue" to "application/clue_info+xml",
+ "cmc" to "application/vnd.cosmocaller",
"cmdf" to "chemical/x-cmdf",
"cml" to "chemical/x-cml",
+ "cmp" to "application/vnd.yellowriver-custom-menu",
+ "cmsc" to "application/cms",
+ "cnd" to "text/jcr-cnd",
"cod" to "application/vnd.rim.cod",
+ "coffee" to "application/vnd.coffeescript",
"com" to "application/x-msdos-program",
+ "copyright" to "text/vnd.debian.copyright",
+ "coswid" to "application/swid+cbor",
"cpa" to "chemical/x-compass",
"cpio" to "application/x-cpio",
+ "cpkg" to "application/vnd.xmpie.cpkg",
+ "cpl" to "application/cpl+xml",
"cpp" to "text/x-c++src",
"cpt" to "image/x-corelphotopaint",
"cr2" to "image/x-canon-cr2",
- "crl" to "application/x-pkcs7-crl",
+ "crl" to "application/pkix-crl",
"crt" to "application/x-x509-ca-cert",
+ "crtr" to "application/vnd.multiad.creator",
"crw" to "image/x-canon-crw",
+ "cryptomator" to "application/vnd.cryptomator.vault",
+ "cryptonote" to "application/vnd.rig.cryptonote",
"csd" to "audio/csound",
"csf" to "chemical/x-cache-csf",
"csh" to "text/x-csh",
+ "csl" to "application/vnd.citationstyles.style+xml",
"csm" to "chemical/x-csml",
"csml" to "chemical/x-csml",
+ "csp" to "application/vnd.commonspace",
+ "csrattrs" to "application/csrattrs",
"css" to "text/css",
+ "cst" to "application/vnd.commonspace",
"csv" to "text/comma-separated-values",
+ "csvs" to "text/csv-schema",
"ctab" to "chemical/x-cactvs-binary",
"ctx" to "chemical/x-ctx",
"cu" to "application/cu-seeme",
"cub" to "chemical/x-gaussian-cube",
+ "cuc" to "application/tamp-community-update-confirm",
"cur" to "image/ico",
+ "curl" to "text/vnd.curl",
+ "cw" to "application/prs.cww",
+ "cwl" to "application/cwl",
+ "cwl.json" to "application/cwl+json",
+ "cww" to "application/prs.cww",
"cxf" to "chemical/x-cxf",
"cxx" to "text/x-c++src",
"d" to "text/x-dsrc",
+ "dae" to "model/vnd.collada+xml",
+ "daf" to "application/vnd.Mobius.DAF",
+ "dart" to "application/vnd.dart",
+ "dataless" to "application/vnd.fdsn.seed",
"davmount" to "application/davmount+xml",
+ "dbf" to "application/vnd.dbf",
+ "dcd" to "application/DCD",
"dcm" to "application/dicom",
"dcr" to "application/x-director",
+ "dd2" to "application/vnd.oma.dd2+xml",
+ "ddd" to "application/vnd.fujixerox.ddd",
"ddeb" to "application/vnd.debian.binary-package",
- "deb" to "application/x-debian-package",
+ "ddf" to "application/vnd.syncml.dmddf+xml",
+ "deb" to "application/vnd.debian.binary-package",
"deploy" to "application/octet-stream",
+ "der" to "application/x-x509-ca-cert",
+ "dfac" to "application/vnd.dreamfactory",
"dfxp" to "application/ttml+xml",
"dif" to "video/dv",
"diff" to "text/plain",
+ "dii" to "application/DII",
+ "dim" to "application/vnd.fastcopy-disk-image",
"dir" to "application/x-director",
+ "dis" to "application/vnd.Mobius.DIS",
+ "dist" to "application/vnd.apple.installer+xml",
+ "distz" to "application/vnd.apple.installer+xml",
+ "dit" to "application/DIT",
+ "dive" to "application/vnd.patentdive",
"djv" to "image/vnd.djvu",
"djvu" to "image/vnd.djvu",
- "dl" to "video/dl",
+ "dl" to "application/vnd.datalog",
"dll" to "application/x-msdos-program",
+ "dls" to "audio/dls",
"dmg" to "application/x-apple-diskimage",
- "dms" to "application/x-dms",
+ "dmp" to "application/vnd.tcpdump.pcap",
+ "dms" to "text/vnd.DMClientScript",
+ "dna" to "application/vnd.dna",
"dng" to "image/x-adobe-dng",
"doc" to "application/msword",
+ "docjson" to "application/vnd.document+json",
"docm" to "application/vnd.ms-word.document.macroEnabled.12",
"docx" to "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- "dot" to "application/msword",
+ "dor" to "model/vnd.gdl",
+ "dot" to "text/vnd.graphviz",
"dotm" to "application/vnd.ms-word.template.macroEnabled.12",
"dotx" to "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
+ "dp" to "application/vnd.osgi.dp",
+ "dpg" to "application/vnd.dpgraph",
+ "dpgraph" to "application/vnd.dpgraph",
+ "dpkg" to "application/vnd.xmpie.dpkg",
+ "dpx" to "image/dpx",
+ "drle" to "image/dicom-rle",
+ "dsc" to "text/prs.lines.tag",
+ "dsm" to "application/vnd.desmume.movie",
+ "dssc" to "application/dssc+der",
+ "dtd" to "application/xml-dtd",
+ "dts" to "audio/vnd.dts",
+ "dtshd" to "audio/vnd.dts.hd",
"dv" to "video/dv",
+ "dvb" to "video/vnd.dvb.file",
+ "dvc" to "application/dvcs",
"dvi" to "application/x-dvi",
+ "dwd" to "application/atsc-dwd+xml",
+ "dwf" to "model/vnd.dwf",
+ "dwg" to "image/vnd.dwg",
"dx" to "chemical/x-jcamp-dx",
+ "dxf" to "image/vnd.dxf",
+ "dxp" to "application/vnd.spotfire.dxp",
"dxr" to "application/x-director",
+ "dzr" to "application/vnd.dzr",
+ "ebuild" to "application/vnd.gentoo.ebuild",
+ "ecelp4800" to "audio/vnd.nuera.ecelp4800",
+ "ecelp7470" to "audio/vnd.nuera.ecelp7470",
+ "ecelp9600" to "audio/vnd.nuera.ecelp9600",
+ "ecig" to "application/vnd.evolv.ecig.settings",
+ "ecigprofile" to "application/vnd.evolv.ecig.profile",
+ "ecigtheme" to "application/vnd.evolv.ecig.theme",
+ "eclass" to "application/vnd.gentoo.eclass",
+ "edm" to "application/vnd.novadigm.EDM",
+ "edx" to "application/vnd.novadigm.EDX",
+ "efi" to "application/efi",
+ "efif" to "application/vnd.picsel",
+ "ei6" to "application/vnd.pg.osasli",
"emb" to "chemical/x-embl-dl-nucleotide",
"embl" to "chemical/x-embl-dl-nucleotide",
+ "emf" to "image/emf",
"eml" to "message/rfc822",
- "ent" to "chemical/x-pdb",
+ "emm" to "application/vnd.ibm.electronic-media",
+ "emma" to "application/emma+xml",
+ "emotionml" to "application/emotionml+xml",
+ "ent" to "application/xml-external-parsed-entity",
+ "entity" to "application/vnd.nervana",
+ "enw" to "audio/EVRCNW",
+ "eol" to "audio/vnd.digital-winds",
"eot" to "application/vnd.ms-fontobject",
+ "ep" to "application/vnd.bluetooth.ep.oob",
"eps" to "application/postscript",
"eps2" to "application/postscript",
"eps3" to "application/postscript",
@@ -152,125 +364,334 @@ private val extensionToMimeTypeMap = mapOf(
"epsi" to "application/postscript",
"epub" to "application/epub+zip",
"erf" to "image/x-epson-erf",
- "es" to "application/ecmascript",
+ "es" to "text/javascript",
+ "es3" to "application/vnd.eszigno3+xml",
+ "esa" to "application/vnd.osgi.subsystem",
+ "esf" to "application/vnd.epson.esf",
+ "espass" to "application/vnd.espass-espass+zip",
+ "et3" to "application/vnd.eszigno3+xml",
"etx" to "text/x-setext",
+ "evb" to "audio/EVRCB",
+ "evc" to "audio/EVRC",
+ "evw" to "audio/EVRCWB",
"exe" to "application/x-msdos-program",
+ "exi" to "application/exi",
+ "exp" to "application/express",
+ "exr" to "image/aces",
+ "ext" to "application/vnd.novadigm.EXT",
"ez" to "application/andrew-inset",
+ "ez2" to "application/vnd.ezpix-album",
+ "ez3" to "application/vnd.ezpix-package",
"f4a" to "audio/mp4",
"f4b" to "audio/mp4",
"f4p" to "audio/mp4",
"f4v" to "video/mp4",
"fb" to "application/x-maker",
"fbdoc" to "application/x-maker",
+ "fbs" to "image/vnd.fastbidsheet",
+ "fcdt" to "application/vnd.adobe.formscentral.fcdt",
"fch" to "chemical/x-gaussian-checkpoint",
"fchk" to "chemical/x-gaussian-checkpoint",
+ "fcs" to "application/vnd.isac.fcs",
+ "fdf" to "application/fdf",
+ "fdt" to "application/fdt+xml",
+ "fe_launch" to "application/vnd.denovo.fcselayout-link",
+ "fg5" to "application/vnd.fujitsu.oasysgp",
"fig" to "application/x-xfig",
+ "finf" to "application/fastinfoset",
+ "fit" to "image/fits",
+ "fits" to "image/fits",
"fl" to "application/x-android-drm-fl",
+ "fla" to "application/vnd.dtg.local.flash",
"flac" to "audio/flac",
+ "flb" to "application/vnd.ficlab.flb+zip",
"fli" to "video/fli",
+ "flo" to "application/vnd.micrografx.flo",
+ "flt" to "text/vnd.ficlab.flt",
"flv" to "video/x-flv",
+ "flw" to "application/vnd.kde.kivio",
+ "flx" to "text/vnd.fmi.flexstor",
+ "fly" to "text/vnd.fly",
"fm" to "application/x-maker",
+ "fo" to "application/vnd.software602.filler.form+xml",
+ "fpx" to "image/vnd.fpx",
"frame" to "application/x-maker",
"frm" to "application/x-maker",
+ "fsc" to "application/vnd.fsc.weblaunch",
+ "fst" to "image/vnd.fst",
+ "ftc" to "application/vnd.fluxtime.clip",
+ "fti" to "application/vnd.anser-web-funds-transfer-initiation",
+ "fts" to "image/fits",
+ "fvt" to "video/vnd.fvt",
+ "fxp" to "application/vnd.adobe.fxp",
+ "fxpl" to "application/vnd.adobe.fxp",
+ "fzs" to "application/vnd.fuzzysheet",
+ "g2w" to "application/vnd.geoplan",
+ "g3w" to "application/vnd.geospace",
+ "gac" to "application/vnd.groove-account",
"gal" to "chemical/x-gaussian-log",
"gam" to "chemical/x-gamess-input",
"gamin" to "chemical/x-gamess-input",
"gan" to "application/x-ganttproject",
"gau" to "chemical/x-gaussian-input",
+ "gbr" to "application/rpki-ghostbusters",
"gcd" to "text/x-pcs-gcd",
"gcf" to "application/x-graphing-calculator",
"gcg" to "chemical/x-gcg8-sequence",
+ "gdl" to "model/vnd.gdl",
+ "gdz" to "application/vnd.familysearch.gedcom+zip",
+ "ged" to "text/vnd.familysearch.gedcom",
"gen" to "chemical/x-genbank",
+ "genozip" to "application/vnd.genozip",
+ "geo" to "application/vnd.dynageo",
+ "geojson" to "application/geo+json",
+ "gex" to "application/vnd.geometry-explorer",
"gf" to "application/x-tex-gf",
+ "gff3" to "text/gff3",
+ "ggb" to "application/vnd.geogebra.file",
+ "ggs" to "application/vnd.geogebra.slides",
+ "ggt" to "application/vnd.geogebra.tool",
+ "ghf" to "application/vnd.groove-help",
"gif" to "image/gif",
+ "gim" to "application/vnd.groove-identity-message",
"gjc" to "chemical/x-gaussian-input",
"gjf" to "chemical/x-gaussian-input",
"gl" to "video/gl",
+ "glb" to "model/gltf-binary",
+ "glbin" to "application/gltf-buffer",
+ "glbuf" to "application/gltf-buffer",
+ "gltf" to "model/gltf+json",
+ "gml" to "application/gml+xml",
"gnumeric" to "application/x-gnumeric",
+ "gph" to "application/vnd.FloGraphIt",
+ "gpkg" to "application/geopackage+sqlite3",
+ "gpkg.tar" to "application/vnd.gentoo.gpkg",
"gpt" to "chemical/x-mopac-graph",
+ "gqf" to "application/vnd.grafeq",
+ "gqs" to "application/vnd.grafeq",
+ "gram" to "application/srgs",
+ "grd" to "application/vnd.gentics.grd+json",
+ "gre" to "application/vnd.geometry-explorer",
+ "grv" to "application/vnd.groove-injector",
+ "grxml" to "application/srgs+xml",
"gsf" to "application/x-font",
+ "gsheet" to "application/urc-grpsheet+xml",
"gsm" to "audio/x-gsm",
"gtar" to "application/x-gtar",
+ "gtm" to "application/vnd.groove-tool-message",
+ "gtw" to "model/vnd.gtw",
+ "gv" to "text/vnd.graphviz",
+ "gxt" to "application/vnd.geonext",
"gz" to "application/gzip",
"h" to "text/x-chdr",
"h++" to "text/x-c++hdr",
+ "hal" to "application/vnd.hal+xml",
+ "hans" to "text/vnd.hans",
+ "hbc" to "application/vnd.hbci",
+ "hbci" to "application/vnd.hbci",
"hdf" to "application/x-hdf",
+ "hdr" to "image/vnd.radiance",
+ "hdt" to "application/vnd.hdt",
"heic" to "image/heic",
"heics" to "image/heic-sequence",
"heif" to "image/heif",
"heifs" to "image/heif-sequence",
+ "hej2" to "image/hej2k",
+ "held" to "application/atsc-held+xml",
+ "hgl" to "text/vnd.hgl",
"hh" to "text/x-c++hdr",
"hif" to "image/heif",
"hin" to "chemical/x-hin",
+ "hpgl" to "application/vnd.hp-HPGL",
+ "hpi" to "application/vnd.hp-hpid",
+ "hpid" to "application/vnd.hp-hpid",
"hpp" to "text/x-c++hdr",
+ "hps" to "application/vnd.hp-hps",
+ "hpub" to "application/prs.hpub+zip",
"hqx" to "application/mac-binhex40",
"hs" to "text/x-haskell",
+ "hsj2" to "image/hsj2",
+ "hsl" to "application/vnd.hsl",
"hta" to "application/hta",
"htc" to "text/x-component",
+ "htke" to "application/vnd.kenameaapp",
"htm" to "text/html",
"html" to "text/html",
+ "hvd" to "application/vnd.yamaha.hv-dic",
+ "hvp" to "application/vnd.yamaha.hv-voice",
+ "hvs" to "application/vnd.yamaha.hv-script",
"hwp" to "application/x-hwp",
"hxx" to "text/x-c++hdr",
+ "i2g" to "application/vnd.intergeo",
+ "ic0" to "application/vnd.commerce-battelle",
+ "ic1" to "application/vnd.commerce-battelle",
+ "ic2" to "application/vnd.commerce-battelle",
+ "ic3" to "application/vnd.commerce-battelle",
+ "ic4" to "application/vnd.commerce-battelle",
+ "ic5" to "application/vnd.commerce-battelle",
+ "ic6" to "application/vnd.commerce-battelle",
+ "ic7" to "application/vnd.commerce-battelle",
+ "ic8" to "application/vnd.commerce-battelle",
"ica" to "application/x-ica",
- "ice" to "x-conference/x-cooltalk",
+ "icc" to "application/vnd.iccprofile",
+ "icd" to "application/vnd.commerce-battelle",
+ "icf" to "application/vnd.commerce-battelle",
+ "icm" to "application/vnd.iccprofile",
"ico" to "image/x-icon",
"ics" to "text/calendar",
- "icz" to "text/calendar",
"ief" to "image/ief",
+ "ifb" to "text/calendar",
+ "ifc" to "application/p21",
+ "ifm" to "application/vnd.shana.informed.formdata",
"iges" to "model/iges",
+ "igl" to "application/vnd.igloader",
+ "igm" to "application/vnd.insors.igm",
+ "ign" to "application/vnd.coreos.ignition+json",
+ "ignition" to "application/vnd.coreos.ignition+json",
"igs" to "model/iges",
+ "igx" to "application/vnd.micrografx.igx",
+ "iif" to "application/vnd.shana.informed.interchange",
"iii" to "application/x-iphone",
+ "imf" to "application/vnd.imagemeter.folder+zip",
+ "imgcal" to "application/vnd.3lightssoftware.imagescal",
+ "imi" to "application/vnd.imagemeter.image+zip",
+ "imp" to "application/vnd.accpac.simply.imp",
+ "ims" to "application/vnd.ms-ims",
+ "imscc" to "application/vnd.ims.imsccv1p1",
"imy" to "audio/imelody",
"info" to "application/x-info",
+ "ink" to "application/inkml+xml",
+ "inkml" to "application/inkml+xml",
"inp" to "chemical/x-gamess-input",
"ins" to "application/x-internet-signup",
+ "iota" to "application/vnd.astraea-software.iota",
+ "ipfix" to "application/ipfix",
+ "ipk" to "application/vnd.shana.informed.package",
+ "ipns-record" to "application/vnd.ipfs.ipns-record",
+ "irm" to "application/vnd.ibm.rights-management",
+ "irp" to "application/vnd.irepository.package+xml",
+ "ism" to "model/vnd.gdl",
"iso" to "application/x-iso9660-image",
"isp" to "application/x-internet-signup",
"ist" to "chemical/x-isostar",
+ "istc" to "application/vnd.veryant.thin",
"istr" to "chemical/x-isostar",
+ "isws" to "application/vnd.veryant.thin",
+ "itp" to "application/vnd.shana.informed.formtemplate",
+ "its" to "application/its+xml",
+ "ivp" to "application/vnd.immervision-ivp",
+ "ivu" to "application/vnd.immervision-ivu",
+ "j2c" to "image/j2c",
+ "j2k" to "image/j2c",
"jad" to "text/vnd.sun.j2me.app-descriptor",
- "jam" to "application/x-jam",
+ "jam" to "application/vnd.jam",
"jar" to "application/java-archive",
"java" to "text/x-java",
"jdx" to "chemical/x-jcamp-dx",
+ "jfif" to "image/jpeg",
+ "jhc" to "image/jphc",
+ "jisp" to "application/vnd.jisp",
+ "jls" to "image/jls",
+ "jlt" to "application/vnd.hp-jlyt",
"jmz" to "application/x-jmol",
"jng" to "image/x-jng",
"jnlp" to "application/x-java-jnlp-file",
+ "joda" to "application/vnd.joost.joda-archive",
"jp2" to "image/jp2",
"jpe" to "image/jpeg",
"jpeg" to "image/jpeg",
"jpf" to "image/jpx",
"jpg" to "image/jpeg",
"jpg2" to "image/jp2",
+ "jpgm" to "image/jpm",
+ "jph" to "image/jph",
+ "jphc" to "image/jphc",
"jpm" to "image/jpm",
"jpx" to "image/jpx",
- "js" to "application/javascript",
+ "jrd" to "application/jrd+json",
+ "js" to "text/javascript",
"json" to "application/json",
+ "json-patch" to "application/json-patch+json",
"jsonld" to "application/ld+json",
- "kar" to "audio/midi",
+ "jsontd" to "application/td+json",
+ "jsontm" to "application/tm+json",
+ "jt" to "model/JT",
+ "jtd" to "text/vnd.esmertec.theme-descriptor",
+ "jxl" to "image/jxl",
+ "jxr" to "image/jxr",
+ "jxra" to "image/jxrA",
+ "jxrs" to "image/jxrS",
+ "jxs" to "image/jxs",
+ "jxsc" to "image/jxsc",
+ "jxsi" to "image/jxsi",
+ "jxss" to "image/jxss",
+ "karbon" to "application/vnd.kde.karbon",
+ "kcm" to "application/vnd.nervana",
"key" to "application/pgp-keys",
+ "keynote" to "application/vnd.apple.keynote",
+ "kfo" to "application/vnd.kde.kformula",
+ "kia" to "application/vnd.kidspiration",
"kil" to "application/x-killustrator",
"kin" to "chemical/x-kinemage",
"kml" to "application/vnd.google-earth.kml+xml",
"kmz" to "application/vnd.google-earth.kmz",
- "kpr" to "application/x-kpresenter",
- "kpt" to "application/x-kpresenter",
- "ksp" to "application/x-kspread",
- "kwd" to "application/x-kword",
- "kwt" to "application/x-kword",
+ "kne" to "application/vnd.Kinar",
+ "knp" to "application/vnd.Kinar",
+ "kom" to "application/vnd.hbci",
+ "kon" to "application/vnd.kde.kontour",
+ "koz" to "audio/vnd.audiokoz",
+ "kpr" to "application/vnd.kde.kpresenter",
+ "kpt" to "application/vnd.kde.kpresenter",
+ "ksp" to "application/vnd.kde.kspread",
+ "ktr" to "application/vnd.kahootz",
+ "ktx" to "image/ktx",
+ "ktx2" to "image/ktx2",
+ "ktz" to "application/vnd.kahootz",
+ "kwd" to "application/vnd.kde.kword",
+ "kwt" to "application/vnd.kde.kword",
+ "l16" to "audio/L16",
+ "las" to "application/vnd.las",
+ "lasjson" to "application/vnd.las.las+json",
+ "lasxml" to "application/vnd.las.las+xml",
"latex" to "application/x-latex",
+ "lbc" to "audio/iLBC",
+ "lbd" to "application/vnd.llamagraphics.life-balance.desktop",
+ "lbe" to "application/vnd.llamagraphics.life-balance.exchange+xml",
+ "lca" to "application/vnd.logipipe.circuit+zip",
+ "lcs" to "application/vnd.logipipe.circuit+zip",
+ "le" to "application/vnd.bluetooth.le.oob",
+ "les" to "application/vnd.hhe.lesson-player",
+ "lgr" to "application/lgr+xml",
"lha" to "application/x-lha",
"lhs" to "text/x-literate-haskell",
+ "lhzd" to "application/vnd.belightsoft.lhzd+zip",
+ "lhzl" to "application/vnd.belightsoft.lhzl+zip",
"lin" to "application/bbolin",
+ "line" to "application/vnd.nebumind.line",
+ "link66" to "application/vnd.route66.link66+xml",
+ "list3820" to "application/vnd.afpc.modca",
+ "listafp" to "application/vnd.afpc.modca",
+ "lmp" to "model/vnd.gdl",
+ "loas" to "audio/usac",
+ "loom" to "application/vnd.loom",
+ "lostsyncxml" to "application/lostsync+xml",
+ "lostxml" to "application/lost+xml",
+ "lpf" to "application/lpf+zip",
"lrc" to "application/lrc",
+ "lrm" to "application/vnd.ms-lrm",
"lsf" to "video/x-la-asf",
"lsx" to "video/x-la-asf",
"ltx" to "text/x-tex",
+ "lvp" to "audio/vnd.lucent.voice",
+ "lwp" to "application/vnd.lotus-wordpro",
+ "lxf" to "application/LXF",
"ly" to "text/x-lilypond",
"lyx" to "application/x-lyx",
"lzh" to "application/x-lzh",
"lzx" to "application/x-lzx",
+ "m" to "application/vnd.wolfram.mathematica.package",
"m1v" to "video/mpeg",
+ "m21" to "application/mp21",
"m2t" to "video/mpeg",
"m2ts" to "video/mp2t",
"m2v" to "video/mpeg",
@@ -281,34 +702,69 @@ private val extensionToMimeTypeMap = mapOf(
"m4b" to "audio/mp4",
"m4p" to "audio/mp4",
"m4r" to "audio/mpeg",
+ "m4s" to "video/iso.segment",
+ "m4u" to "video/vnd.mpegurl",
"m4v" to "video/mp4",
+ "ma" to "application/mathematica",
+ "mads" to "application/mads+xml",
+ "maei" to "application/mmt-aei+xml",
+ "mag" to "application/vnd.ecowin.chart",
+ "mail" to "message/rfc822",
"maker" to "application/x-maker",
"man" to "application/x-troff-man",
- "manifest" to "application/x-ms-manifest",
+ "manifest" to "text/cache-manifest",
"markdown" to "text/markdown",
+ "mb" to "application/mathematica",
+ "mbk" to "application/vnd.Mobius.MBK",
"mbox" to "application/mbox",
+ "mbsdf" to "application/vnd.mdl-mbsdf",
+ "mc1" to "application/vnd.medcalcdata",
+ "mc2" to "text/vnd.senx.warpscript",
+ "mcd" to "application/vnd.mcd",
"mcif" to "chemical/x-mmcif",
"mcm" to "chemical/x-macmolecule",
"md" to "text/markdown",
"mdb" to "application/msaccess",
+ "mdc" to "application/vnd.marlin.drm.mdcf",
+ "mdi" to "image/vnd.ms-modi",
+ "mdl" to "application/vnd.mdl",
"me" to "application/x-troff-me",
"mesh" to "model/mesh",
- "mid" to "audio/midi",
- "midi" to "audio/midi",
- "mif" to "application/x-mif",
- "mjs" to "application/javascript",
+ "meta4" to "application/metalink4+xml",
+ "mets" to "application/mets+xml",
+ "mf4" to "application/MF4",
+ "mfm" to "application/vnd.mfmp",
+ "mft" to "application/rpki-manifest",
+ "mgp" to "application/vnd.osgeo.mapguide.package",
+ "mgz" to "application/vnd.proteus.magazine",
+ "mhas" to "audio/mhas",
+ "mid" to "audio/sp-midi",
+ "mif" to "application/vnd.mif",
+ "miz" to "text/mizar",
+ "mj2" to "video/mj2",
+ "mjp2" to "video/mj2",
+ "mjs" to "text/javascript",
"mka" to "audio/x-matroska",
"mkv" to "video/x-matroska",
+ "ml2" to "application/vnd.sybyl.mol2",
+ "mlp" to "audio/vnd.dolby.mlp",
"mm" to "application/x-freemind",
- "mmd" to "chemical/x-macromodel-input",
+ "mmd" to "application/vnd.chipnuts.karaoke-mmd",
+ "mmdb" to "application/vnd.maxmind.maxmind-db",
"mmf" to "application/vnd.smaf",
- "mml" to "text/mathml",
+ "mml" to "application/mathml+xml",
"mmod" to "chemical/x-macromodel-input",
+ "mmr" to "image/vnd.fujixerox.edmics-mmr",
"mng" to "video/x-mng",
"mobi" to "application/x-mobipocket-ebook",
"moc" to "text/x-moc",
+ "mod" to "application/xml-dtd",
+ "model-inter" to "application/vnd.vd-study",
+ "modl" to "application/vnd.modl",
+ "mods" to "application/mods+xml",
"mol" to "chemical/x-mdl-molfile",
- "mol2" to "chemical/x-mol2",
+ "mol2" to "application/vnd.sybyl.mol2",
+ "moml" to "model/vnd.moml+xml",
"moo" to "chemical/x-mopac-out",
"mop" to "chemical/x-mopac-input",
"mopcrt" to "chemical/x-mopac-input",
@@ -317,44 +773,111 @@ private val extensionToMimeTypeMap = mapOf(
"mp1" to "audio/mpeg",
"mp1v" to "video/mpeg",
"mp2" to "audio/mpeg",
+ "mp21" to "application/mp21",
"mp2v" to "video/mpeg",
"mp3" to "audio/mpeg",
"mp4" to "video/mp4",
"mp4v" to "video/mp4",
"mpa" to "audio/mpeg",
"mpc" to "chemical/x-mopac-input",
+ "mpd" to "application/dash+xml",
+ "mpdd" to "application/dashdelta",
"mpe" to "video/mpeg",
"mpeg" to "video/mpeg",
"mpeg1" to "video/mpeg",
"mpeg2" to "video/mpeg",
"mpeg4" to "video/mp4",
"mpega" to "audio/mpeg",
+ "mpf" to "text/vnd.ms-mediapackage",
"mpg" to "video/mpeg",
+ "mpg4" to "video/mp4",
"mpga" to "audio/mpeg",
"mph" to "application/x-comsol",
+ "mpkg" to "application/vnd.apple.installer+xml",
+ "mpm" to "application/vnd.blueice.multipass",
+ "mpn" to "application/vnd.mophun.application",
+ "mpp" to "application/vnd.ms-project",
+ "mpt" to "application/vnd.ms-project",
"mpv" to "video/x-matroska",
"mpv1" to "video/mpeg",
"mpv2" to "video/mpeg",
+ "mpw" to "application/vnd.exstream-empower+zip",
+ "mpy" to "application/vnd.ibm.MiniPay",
+ "mqy" to "application/vnd.Mobius.MQY",
+ "mrc" to "application/marc",
+ "mrcx" to "application/marcxml+xml",
"ms" to "application/x-troff-ms",
+ "msa" to "application/vnd.msa-disk-image",
+ "msd" to "application/vnd.fdsn.mseed",
+ "mseed" to "application/vnd.fdsn.mseed",
+ "mseq" to "application/vnd.mseq",
+ "msf" to "application/vnd.epson.msf",
"msh" to "model/mesh",
"msi" to "application/x-msi",
+ "msl" to "application/vnd.Mobius.MSL",
+ "msm" to "model/vnd.gdl",
"msp" to "application/octet-stream",
+ "msty" to "application/vnd.muvee.style",
"msu" to "application/octet-stream",
+ "mtl" to "model/mtl",
"mts" to "video/mp2t",
+ "multitrack" to "audio/vnd.presonus.multitrack",
+ "mus" to "application/vnd.musician",
+ "musd" to "application/mmt-usd+xml",
"mvb" to "chemical/x-mopac-vib",
+ "mvt" to "application/vnd.mapbox-vector-tile",
+ "mwc" to "application/vnd.dpgraph",
+ "mwf" to "application/vnd.MFER",
"mxf" to "application/mxf",
+ "mxi" to "application/vnd.vd-study",
+ "mxl" to "application/vnd.recordare.musicxml",
"mxmf" to "audio/mobile-xmf",
+ "mxml" to "application/xv+xml",
+ "mxs" to "application/vnd.triscape.mxs",
"mxu" to "video/vnd.mpegurl",
- "nb" to "application/mathematica",
- "nbp" to "application/mathematica",
+ "n3" to "text/n3",
+ "nb" to "application/vnd.wolfram.mathematica",
+ "nbp" to "application/vnd.wolfram.player",
"nc" to "application/x-netcdf",
+ "ndc" to "application/vnd.osa.netdeploy",
+ "ndl" to "application/vnd.lotus-notes",
+ "nds" to "application/vnd.nintendo.nitro.rom",
+ "nebul" to "application/vnd.nebumind.line",
"nef" to "image/x-nikon-nef",
+ "ngdat" to "application/vnd.nokia.n-gage.data",
+ "nim" to "video/vnd.nokia.interleaved-multimedia",
+ "nimn" to "application/vnd.nimn",
+ "nitf" to "application/vnd.nitf",
+ "nlu" to "application/vnd.neurolanguage.nlu",
+ "nml" to "application/vnd.enliven",
+ "nnd" to "application/vnd.noblenet-directory",
+ "nns" to "application/vnd.noblenet-sealer",
+ "nnw" to "application/vnd.noblenet-web",
+ "notebook" to "application/vnd.smart.notebook",
+ "nq" to "application/n-quads",
"nrw" to "image/x-nikon-nrw",
+ "ns2" to "application/vnd.lotus-notes",
+ "ns3" to "application/vnd.lotus-notes",
+ "ns4" to "application/vnd.lotus-notes",
+ "nsf" to "application/vnd.lotus-notes",
+ "nsg" to "application/vnd.lotus-notes",
+ "nsh" to "application/vnd.lotus-notes",
+ "nt" to "application/n-triples",
+ "ntf" to "application/vnd.lotus-notes",
+ "numbers" to "application/vnd.apple.numbers",
"nwc" to "application/x-nwc",
"o" to "application/x-object",
- "oda" to "application/oda",
- "odb" to "application/vnd.oasis.opendocument.database",
+ "oa2" to "application/vnd.fujitsu.oasys2",
+ "oa3" to "application/vnd.fujitsu.oasys3",
+ "oas" to "application/vnd.fujitsu.oasys",
+ "ob" to "application/vnd.1ob",
+ "obg" to "application/vnd.openblox.game-binary",
+ "obgx" to "application/vnd.openblox.game+xml",
+ "obj" to "model/obj",
+ "oda" to "application/ODA",
+ "odb" to "application/vnd.oasis.opendocument.base",
"odc" to "application/vnd.oasis.opendocument.chart",
+ "odd" to "application/tei+xml",
"odf" to "application/vnd.oasis.opendocument.formula",
"odg" to "application/vnd.oasis.opendocument.graphics",
"odi" to "application/vnd.oasis.opendocument.image",
@@ -362,225 +885,611 @@ private val extensionToMimeTypeMap = mapOf(
"odp" to "application/vnd.oasis.opendocument.presentation",
"ods" to "application/vnd.oasis.opendocument.spreadsheet",
"odt" to "application/vnd.oasis.opendocument.text",
+ "odx" to "application/ODX",
+ "oeb" to "application/vnd.openeye.oeb",
"oga" to "audio/ogg",
+ "ogex" to "model/vnd.opengex",
"ogg" to "audio/ogg",
"ogv" to "video/ogg",
"ogx" to "application/ogg",
"old" to "application/x-trash",
+ "omg" to "audio/ATRAC3",
"one" to "application/onenote",
"onepkg" to "application/onenote",
"onetmp" to "application/onenote",
"onetoc2" to "application/onenote",
"opf" to "application/oebps-package+xml",
+ "oprc" to "application/vnd.palm",
"opus" to "audio/ogg",
+ "or2" to "application/vnd.lotus-organizer",
+ "or3" to "application/vnd.lotus-organizer",
"orc" to "audio/csound",
"orf" to "image/x-olympus-orf",
+ "org" to "application/vnd.lotus-organizer",
+ "orq" to "application/ocsp-request",
+ "ors" to "application/ocsp-response",
+ "osf" to "application/vnd.yamaha.openscoreformat",
+ "osm" to "application/vnd.openstreetmap.data+xml",
"ota" to "application/vnd.android.ota",
- "otf" to "font/ttf",
+ "otc" to "application/vnd.oasis.opendocument.chart-template",
+ "otf" to "font/otf",
"otg" to "application/vnd.oasis.opendocument.graphics-template",
"oth" to "application/vnd.oasis.opendocument.text-web",
+ "oti" to "application/vnd.oasis.opendocument.image-template",
+ "otm" to "application/vnd.oasis.opendocument.text-master-template",
"otp" to "application/vnd.oasis.opendocument.presentation-template",
"ots" to "application/vnd.oasis.opendocument.spreadsheet-template",
"ott" to "application/vnd.oasis.opendocument.text-template",
+ "ovl" to "application/vnd.afpc.modca-overlay",
+ "oxlicg" to "application/vnd.oxli.countgraph",
+ "oxps" to "application/oxps",
+ "oxt" to "application/vnd.openofficeorg.extension",
"oza" to "application/x-oz-application",
"p" to "text/x-pascal",
+ "p10" to "application/pkcs10",
"p12" to "application/x-pkcs12",
+ "p21" to "application/p21",
+ "p2p" to "application/vnd.wfa.p2p",
+ "p7c" to "application/pkcs7-mime",
+ "p7m" to "application/pkcs7-mime",
"p7r" to "application/x-pkcs7-certreqresp",
+ "p7s" to "application/pkcs7-signature",
+ "p7z" to "application/pkcs7-mime",
+ "p8" to "application/pkcs8",
+ "p8e" to "application/pkcs8-encrypted",
"pac" to "application/x-ns-proxy-autoconfig",
+ "package" to "application/vnd.autopackage",
+ "pages" to "application/vnd.apple.pages",
"pas" to "text/x-pascal",
"pat" to "image/x-coreldrawpattern",
"patch" to "text/x-diff",
+ "paw" to "application/vnd.pawaafile",
+ "pbd" to "application/vnd.powerbuilder6",
"pbm" to "image/x-portable-bitmap",
"pcap" to "application/vnd.tcpdump.pcap",
"pcf" to "application/x-font",
"pcf.Z" to "application/x-font-pcf",
- "pcx" to "image/pcx",
+ "pcl" to "application/vnd.hp-PCL",
+ "pcx" to "image/vnd.zbrush.pcx",
"pdb" to "chemical/x-pdb",
"pdf" to "application/pdf",
+ "pdx" to "application/PDX",
"pef" to "image/x-pentax-pef",
"pem" to "application/x-pem-file",
"pfa" to "application/x-font",
"pfb" to "application/x-font",
"pfr" to "application/font-tdpfr",
"pfx" to "application/x-pkcs12",
+ "pgb" to "image/vnd.globalgraphics.pgb",
"pgm" to "image/x-portable-graymap",
- "pgn" to "application/x-chess-pgn",
+ "pgn" to "application/vnd.chess-pgn",
"pgp" to "application/pgp-signature",
"phps" to "text/text",
+ "pil" to "application/vnd.piaccess.application-licence",
"pk" to "application/x-tex-pk",
+ "pkd" to "application/vnd.hbci",
+ "pkg" to "application/vnd.apple.installer+xml",
+ "pki" to "application/pkixcmp",
+ "pkipath" to "application/pkix-pkipath",
+ "pkpass" to "application/vnd.apple.pkpass",
+ "pkpasses" to "application/vnd.apple.pkpasses",
"pl" to "text/x-perl",
+ "plb" to "application/vnd.3gpp.pic-bw-large",
+ "plc" to "application/vnd.Mobius.PLC",
+ "plf" to "application/vnd.pocketlearn",
+ "plj" to "audio/vnd.everad.plj",
+ "plp" to "application/vnd.panoply",
"pls" to "audio/x-scpls",
"pm" to "text/x-perl",
+ "pml" to "application/vnd.ctc-posml",
"png" to "image/png",
"pnm" to "image/x-portable-anymap",
"po" to "text/plain",
+ "portpkg" to "application/vnd.macports.portpkg",
"pot" to "application/vnd.ms-powerpoint",
"potm" to "application/vnd.ms-powerpoint.template.macroEnabled.12",
"potx" to "application/vnd.openxmlformats-officedocument.presentationml.template",
"ppam" to "application/vnd.ms-powerpoint.addin.macroEnabled.12",
+ "ppd" to "application/vnd.cups-ppd",
+ "ppkg" to "application/vnd.xmpie.ppkg",
"ppm" to "image/x-portable-pixmap",
"pps" to "application/vnd.ms-powerpoint",
"ppsm" to "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
"ppsx" to "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"ppt" to "application/vnd.ms-powerpoint",
"pptm" to "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
+ "ppttc" to "application/vnd.think-cell.ppttc+json",
"pptx" to "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ "pqa" to "application/vnd.palm",
"prc" to "application/x-mobipocket-ebook",
+ "pre" to "application/vnd.lotus-freelance",
+ "preminet" to "application/vnd.preminet",
"prf" to "application/pics-rules",
+ "provn" to "text/provenance-notation",
+ "provx" to "application/provenance+xml",
"prt" to "chemical/x-ncbi-asn1-ascii",
+ "prz" to "application/vnd.lotus-freelance",
"ps" to "application/postscript",
+ "psb" to "application/vnd.3gpp.pic-bw-small",
"psd" to "image/x-photoshop",
+ "pseg3820" to "application/vnd.afpc.modca",
+ "psfs" to "application/vnd.psfs",
+ "psg" to "application/vnd.afpc.modca-pagesegment",
+ "psid" to "audio/prs.sid",
+ "pskcxml" to "application/pskc+xml",
+ "pt" to "application/vnd.snesdev-page-table",
+ "pti" to "image/prs.pti",
+ "ptid" to "application/vnd.pvi.ptid1",
+ "ptrom" to "application/vnd.snesdev-page-table",
+ "pub" to "application/vnd.exstream-package",
+ "pvb" to "application/vnd.3gpp.pic-bw-var",
+ "pwn" to "application/vnd.3M.Post-it-Notes",
"py" to "text/x-python",
+ "pya" to "audio/vnd.ms-playready.media.pya",
"pyc" to "application/x-python-code",
"pyo" to "application/x-python-code",
+ "pyox" to "model/vnd.pytha.pyox",
+ "pyv" to "video/vnd.ms-playready.media.pyv",
+ "qam" to "application/vnd.epson.quickanime",
+ "qbo" to "application/vnd.intu.qbo",
+ "qca" to "application/vnd.ericsson.quickcall",
+ "qcall" to "application/vnd.ericsson.quickcall",
+ "qcp" to "audio/EVRC-QCP",
+ "qfx" to "application/vnd.intu.qfx",
"qgs" to "application/x-qgis",
+ "qps" to "application/vnd.publishare-delta-tree",
"qt" to "video/quicktime",
"qtl" to "application/x-quicktimeplayer",
+ "quiz" to "application/vnd.quobject-quoxdocument",
+ "quox" to "application/vnd.quobject-quoxdocument",
+ "qvd" to "application/vnd.theqvd",
+ "qwd" to "application/vnd.Quark.QuarkXPress",
+ "qwt" to "application/vnd.Quark.QuarkXPress",
+ "qxb" to "application/vnd.Quark.QuarkXPress",
+ "qxd" to "application/vnd.Quark.QuarkXPress",
+ "qxl" to "application/vnd.Quark.QuarkXPress",
+ "qxt" to "application/vnd.Quark.QuarkXPress",
"ra" to "audio/x-pn-realaudio",
"raf" to "image/x-fuji-raf",
"ram" to "audio/x-pn-realaudio",
- "rar" to "application/rar",
+ "rapd" to "application/route-apd+xml",
+ "rar" to "application/vnd.rar",
"ras" to "image/x-cmu-raster",
"rb" to "application/x-ruby",
+ "rcprofile" to "application/vnd.ipunplugged.rcprofile",
+ "rct" to "application/prs.nprend",
"rd" to "chemical/x-mdl-rdfile",
"rdf" to "application/rdf+xml",
+ "rdf-crypt" to "application/prs.rdf-xml-crypt",
"rdp" to "application/x-rdp",
+ "rdz" to "application/vnd.data-vision.rdz",
+ "relo" to "application/p2p-overlay+xml",
+ "reload" to "application/vnd.resilient.logic",
+ "rep" to "application/vnd.businessobjects",
+ "request" to "application/vnd.nervana",
+ "rfcxml" to "application/rfc+xml",
"rgb" to "image/x-rgb",
+ "rgbe" to "image/vnd.radiance",
+ "rif" to "application/reginfo+xml",
+ "rip" to "audio/vnd.rip",
+ "rl" to "application/resource-lists+xml",
+ "rlc" to "image/vnd.fujixerox.edmics-rlc",
+ "rld" to "application/resource-lists-diff+xml",
+ "rlm" to "application/vnd.resilient.logic",
"rm" to "audio/x-pn-realaudio",
- "roff" to "application/x-troff",
+ "rms" to "application/vnd.jcp.javame.midlet-rms",
+ "rnc" to "application/relax-ng-compact-syntax",
+ "rnd" to "application/prs.nprend",
+ "roa" to "application/rpki-roa",
+ "roff" to "text/troff",
"ros" to "chemical/x-rosdal",
+ "rp9" to "application/vnd.cloanto.rp9",
"rpm" to "application/x-redhat-package-manager",
+ "rpss" to "application/vnd.nokia.radio-presets",
+ "rpst" to "application/vnd.nokia.radio-preset",
+ "rq" to "application/sparql-query",
+ "rs" to "application/rls-services+xml",
+ "rsat" to "application/atsc-rsat+xml",
+ "rsheet" to "application/urc-ressheet+xml",
+ "rsm" to "model/vnd.gdl",
"rss" to "application/rss+xml",
+ "rst" to "text/prs.fallenstein.rst",
"rtf" to "text/rtf",
"rtttl" to "audio/midi",
"rtx" to "audio/midi",
+ "rusd" to "application/route-usd+xml",
"rw2" to "image/x-panasonic-rw2",
"rxn" to "chemical/x-mdl-rxnfile",
+ "rxt" to "application/vnd.medicalholodeck.recordxr",
+ "s11" to "video/vnd.sealed.mpeg1",
+ "s14" to "video/vnd.sealed.mpeg4",
+ "s1a" to "application/vnd.sealedmedia.softseal.pdf",
+ "s1e" to "application/vnd.sealed.xls",
+ "s1g" to "image/vnd.sealedmedia.softseal.gif",
+ "s1h" to "application/vnd.sealedmedia.softseal.html",
+ "s1j" to "image/vnd.sealedmedia.softseal.jpg",
+ "s1m" to "audio/vnd.sealedmedia.softseal.mpeg",
+ "s1n" to "image/vnd.sealed.png",
+ "s1p" to "application/vnd.sealed.ppt",
+ "s1q" to "video/vnd.sealedmedia.softseal.mov",
+ "s1w" to "application/vnd.sealed.doc",
+ "s3df" to "application/vnd.sealed.3df",
+ "sac" to "application/tamp-sequence-adjust-confirm",
+ "saf" to "application/vnd.yamaha.smaf-audio",
+ "sam" to "application/vnd.lotus-wordpro",
+ "sarif" to "application/sarif+json",
+ "sarif-external-properties" to "application/sarif-external-properties+json",
+ "sarif-external-properties.json" to "application/sarif-external-properties+json",
+ "sarif.json" to "application/sarif+json",
+ "sc" to "application/vnd.ibm.secure-container",
"scala" to "text/x-scala",
+ "scd" to "application/vnd.scribus",
"sce" to "application/x-scilab",
"sci" to "application/x-scilab",
+ "scim" to "application/scim+json",
+ "scl" to "application/vnd.sycle+xml",
+ "scld" to "application/vnd.doremir.scorecloud-binary-document",
+ "scm" to "application/vnd.lotus-screencam",
"sco" to "audio/csound",
+ "scq" to "application/scvp-cv-request",
"scr" to "application/x-silverlight",
- "sct" to "text/scriptlet",
+ "scs" to "application/scvp-cv-response",
+ "scsf" to "application/vnd.sealed.csf",
"sd" to "chemical/x-mdl-sdfile",
"sd2" to "audio/x-sd2",
"sda" to "application/vnd.stardivision.draw",
"sdc" to "application/vnd.stardivision.calc",
"sdd" to "application/vnd.stardivision.impress",
"sdf" to "chemical/x-mdl-sdfile",
+ "sdkd" to "application/vnd.solent.sdkm+xml",
+ "sdkm" to "application/vnd.solent.sdkm+xml",
+ "sdo" to "application/vnd.sealed.doc",
+ "sdoc" to "application/vnd.sealed.doc",
"sdp" to "application/vnd.stardivision.impress",
"sds" to "application/vnd.stardivision.chart",
"sdw" to "application/vnd.stardivision.writer",
+ "see" to "application/vnd.seemail",
+ "seed" to "application/vnd.fdsn.seed",
+ "sem" to "application/vnd.sealed.eml",
+ "sema" to "application/vnd.sema",
+ "semd" to "application/vnd.semd",
+ "semf" to "application/vnd.semf",
+ "seml" to "application/vnd.sealed.eml",
+ "senml" to "application/senml+json",
+ "senml-etchc" to "application/senml-etch+cbor",
+ "senml-etchj" to "application/senml-etch+json",
+ "senmlc" to "application/senml+cbor",
+ "senmle" to "application/senml-exi",
+ "senmlx" to "application/senml+xml",
+ "sensml" to "application/sensml+json",
+ "sensmlc" to "application/sensml+cbor",
+ "sensmle" to "application/sensml-exi",
+ "sensmlx" to "application/sensml+xml",
"ser" to "application/java-serialized-object",
+ "sfc" to "application/vnd.nintendo.snes.rom",
"sfd" to "application/vnd.font-fontforge-sfd",
+ "sfd-hdstx" to "application/vnd.hydrostatix.sof-data",
+ "sfs" to "application/vnd.spotfire.sfs",
"sfv" to "text/x-sfv",
"sgf" to "application/x-go-sgf",
+ "sgi" to "image/vnd.sealedmedia.softseal.gif",
+ "sgif" to "image/vnd.sealedmedia.softseal.gif",
"sgl" to "application/vnd.stardivision.writer-global",
+ "sgm" to "text/SGML",
+ "sgml" to "text/SGML",
"sh" to "text/x-sh",
+ "shaclc" to "text/shaclc",
"shar" to "application/x-shar",
+ "shc" to "text/shaclc",
+ "shex" to "text/shex",
+ "shf" to "application/shf+xml",
"shp" to "application/x-qgis",
"shtml" to "text/html",
"shx" to "application/x-qgis",
+ "si" to "text/vnd.wap.si",
+ "sic" to "application/vnd.wap.sic",
"sid" to "audio/prs.sid",
+ "sieve" to "application/sieve",
"sig" to "application/pgp-signature",
"sik" to "application/x-trash",
"silo" to "model/mesh",
+ "sipa" to "application/vnd.smintio.portals.archive",
"sis" to "application/vnd.symbian.install",
- "sisx" to "x-epoc/x-sisx-app",
"sit" to "application/x-stuffit",
"sitx" to "application/x-stuffit",
- "skd" to "application/x-koan",
- "skm" to "application/x-koan",
- "skp" to "application/x-koan",
- "skt" to "application/x-koan",
+ "siv" to "application/sieve",
+ "sjp" to "image/vnd.sealedmedia.softseal.jpg",
+ "sjpg" to "image/vnd.sealedmedia.softseal.jpg",
+ "skd" to "application/vnd.koan",
+ "skm" to "application/vnd.koan",
+ "skp" to "application/vnd.koan",
+ "skt" to "application/vnd.koan",
+ "sl" to "text/vnd.wap.sl",
+ "sla" to "application/vnd.scribus",
+ "slaz" to "application/vnd.scribus",
+ "slc" to "application/vnd.wap.slc",
"sldm" to "application/vnd.ms-powerpoint.slide.macroEnabled.12",
"sldx" to "application/vnd.openxmlformats-officedocument.presentationml.slide",
+ "sls" to "application/route-s-tsid+xml",
+ "slt" to "application/vnd.epson.salt",
+ "sm" to "application/vnd.stepmania.stepchart",
+ "smc" to "application/vnd.nintendo.snes.rom",
"smf" to "audio/sp-midi",
+ "smh" to "application/vnd.sealed.mht",
+ "smht" to "application/vnd.sealed.mht",
"smi" to "application/smil+xml",
"smil" to "application/smil+xml",
+ "smk" to "video/vnd.radgamettools.smacker",
+ "sml" to "application/smil+xml",
+ "smo" to "video/vnd.sealedmedia.softseal.mov",
+ "smov" to "video/vnd.sealedmedia.softseal.mov",
+ "smp" to "audio/vnd.sealedmedia.softseal.mpeg",
+ "smp3" to "audio/vnd.sealedmedia.softseal.mpeg",
+ "smpg" to "video/vnd.sealed.mpeg1",
+ "sms" to "application/vnd.3gpp2.sms",
+ "smv" to "audio/SMV",
+ "smzip" to "application/vnd.stepmania.package",
"snd" to "audio/basic",
+ "soa" to "text/dns",
+ "soc" to "application/sgml-open-catalog",
+ "sofa" to "audio/sofa",
+ "sos" to "text/vnd.sosi",
"spc" to "chemical/x-galactic-spc",
- "spl" to "application/x-futuresplash",
+ "spd" to "application/vnd.sealedmedia.softseal.pdf",
+ "spdf" to "application/vnd.sealedmedia.softseal.pdf",
+ "spdx" to "text/spdx",
+ "spdx.json" to "application/spdx+json",
+ "spf" to "application/vnd.yamaha.smaf-phrase",
+ "spl" to "application/futuresplash",
+ "spn" to "image/vnd.sealed.png",
+ "spng" to "image/vnd.sealed.png",
+ "spo" to "text/vnd.in3d.spot",
+ "spot" to "text/vnd.in3d.spot",
+ "spp" to "application/scvp-vp-response",
+ "sppt" to "application/vnd.sealed.ppt",
+ "spq" to "application/scvp-vp-request",
"spx" to "audio/ogg",
- "sql" to "application/x-sql",
+ "sql" to "application/sql",
+ "sqlite" to "application/vnd.sqlite3",
+ "sqlite3" to "application/vnd.sqlite3",
+ "sr" to "application/vnd.sigrok.session",
"src" to "application/x-wais-source",
"srt" to "application/x-subrip",
+ "sru" to "application/sru+xml",
"srw" to "image/x-samsung-srw",
+ "srx" to "application/sparql-results+xml",
+ "sse" to "application/vnd.kodak-descriptor",
+ "ssf" to "application/vnd.epson.ssf",
+ "ssml" to "application/ssml+xml",
+ "ssv" to "application/vnd.shade-save-file",
+ "ssvc" to "application/vnd.crypto-shade-file",
+ "ssw" to "video/vnd.sealed.swf",
+ "sswf" to "video/vnd.sealed.swf",
+ "st" to "application/vnd.sailingtracker.track",
"stc" to "application/vnd.sun.xml.calc.template",
"std" to "application/vnd.sun.xml.draw.template",
+ "step" to "model/step",
+ "stf" to "application/vnd.wt.stf",
"sti" to "application/vnd.sun.xml.impress.template",
+ "stif" to "application/vnd.sealed.tiff",
+ "stix" to "application/stix+json",
+ "stk" to "application/hyperstudio",
"stl" to "application/vnd.ms-pki.stl",
+ "stml" to "application/vnd.sealedmedia.softseal.html",
+ "stp" to "model/step",
+ "stpnc" to "application/p21",
+ "stpx" to "model/step+xml",
+ "stpxz" to "model/step-xml+zip",
+ "stpz" to "model/step+zip",
+ "str" to "application/vnd.pg.format",
+ "study-inter" to "application/vnd.vd-study",
"stw" to "application/vnd.sun.xml.writer.template",
"sty" to "text/x-tex",
+ "sus" to "application/vnd.sus-calendar",
+ "susp" to "application/vnd.sus-calendar",
"sv4cpio" to "application/x-sv4cpio",
"sv4crc" to "application/x-sv4crc",
+ "svc" to "application/vnd.dvb.service",
"svg" to "image/svg+xml",
"svgz" to "image/svg+xml",
"sw" to "chemical/x-swissprot",
- "swf" to "application/x-shockwave-flash",
- "swfl" to "application/x-shockwave-flash",
+ "swf" to "application/vnd.adobe.flash.movie",
+ "swi" to "application/vnd.aristanetworks.swi",
+ "swidtag" to "application/swid+xml",
"sxc" to "application/vnd.sun.xml.calc",
"sxd" to "application/vnd.sun.xml.draw",
"sxg" to "application/vnd.sun.xml.writer.global",
"sxi" to "application/vnd.sun.xml.impress",
+ "sxl" to "application/vnd.sealed.xls",
+ "sxls" to "application/vnd.sealed.xls",
"sxm" to "application/vnd.sun.xml.math",
"sxw" to "application/vnd.sun.xml.writer",
- "t" to "application/x-troff",
+ "sy2" to "application/vnd.sybyl.mol2",
+ "syft.json" to "application/vnd.syft+json",
+ "t" to "text/troff",
+ "tag" to "text/prs.lines.tag",
+ "taglet" to "application/vnd.mynfc",
+ "tam" to "application/vnd.onepager",
+ "tamp" to "application/vnd.onepagertamp",
+ "tamx" to "application/vnd.onepagertamx",
+ "tao" to "application/vnd.tao.intent-module-archive",
+ "tap" to "image/vnd.tencent.tap",
"tar" to "application/x-tar",
+ "tat" to "application/vnd.onepagertat",
+ "tatp" to "application/vnd.onepagertatp",
+ "tatx" to "application/vnd.onepagertatx",
+ "tau" to "application/tamp-apex-update",
"taz" to "application/x-gtar-compressed",
+ "tcap" to "application/vnd.3gpp2.tcap",
"tcl" to "text/x-tcl",
+ "tcu" to "application/tamp-community-update",
+ "td" to "application/urc-targetdesc+xml",
+ "teacher" to "application/vnd.smart.teacher",
+ "tei" to "application/tei+xml",
+ "teiCorpus" to "application/tei+xml",
+ "ter" to "application/tamp-error",
"tex" to "text/x-tex",
"texi" to "application/x-texinfo",
"texinfo" to "application/x-texinfo",
"text" to "text/plain",
+ "tfi" to "application/thraud+xml",
+ "tfx" to "image/tiff-fx",
"tgf" to "chemical/x-mdl-tgf",
"tgz" to "application/x-gtar-compressed",
"thmx" to "application/vnd.ms-officetheme",
"tif" to "image/tiff",
"tiff" to "image/tiff",
"tk" to "text/x-tcl",
+ "tlclient" to "application/vnd.cendio.thinlinc.clientconf",
"tm" to "text/texmacs",
+ "tm.json" to "application/tm+json",
+ "tm.jsonld" to "application/tm+json",
+ "tmo" to "application/vnd.tmobile-livetv",
+ "tnef" to "application/vnd.ms-tnef",
+ "tnf" to "application/vnd.ms-tnef",
"torrent" to "application/x-bittorrent",
- "tr" to "application/x-troff",
+ "tpl" to "application/vnd.groove-tool-template",
+ "tpt" to "application/vnd.trid.tpt",
+ "tr" to "text/troff",
+ "tra" to "application/vnd.trueapp",
+ "tree" to "application/vnd.rainstor.data",
+ "trig" to "application/trig",
"ts" to "video/mp2ts",
+ "tsa" to "application/tamp-sequence-adjust",
+ "tsd" to "application/timestamped-data",
"tsp" to "application/dsptype",
+ "tsq" to "application/timestamp-query",
+ "tsr" to "application/timestamp-reply",
+ "tst" to "application/vnd.etsi.timestamp-token",
"tsv" to "text/tab-separated-values",
"ttc" to "font/collection",
"ttf" to "font/ttf",
"ttl" to "text/turtle",
"ttml" to "application/ttml+xml",
+ "tuc" to "application/tamp-update-confirm",
+ "tur" to "application/tamp-update",
+ "twd" to "application/vnd.SimTech-MindMapper",
+ "twds" to "application/vnd.SimTech-MindMapper",
+ "txd" to "application/vnd.genomatix.tuxedo",
+ "txf" to "application/vnd.Mobius.TXF",
"txt" to "text/plain",
- "udeb" to "application/x-debian-package",
- "uls" to "text/iuls",
+ "u3d" to "model/u3d",
+ "u8dsn" to "message/global-delivery-status",
+ "u8hdr" to "message/global-headers",
+ "u8mdn" to "message/global-disposition-notification",
+ "u8msg" to "message/global",
+ "udeb" to "application/vnd.debian.binary-package",
+ "ufd" to "application/vnd.ufdl",
+ "ufdl" to "application/vnd.ufdl",
+ "uis" to "application/urc-uisocketdesc+xml",
+ "umj" to "application/vnd.umajin",
+ "unityweb" to "application/vnd.unity",
+ "uo" to "application/vnd.uoml+xml",
+ "uoml" to "application/vnd.uoml+xml",
+ "upa" to "application/vnd.hbci",
+ "uri" to "text/uri-list",
+ "urim" to "application/vnd.uri-map",
+ "urimap" to "application/vnd.uri-map",
+ "uris" to "text/uri-list",
+ "usda" to "model/vnd.usda",
+ "usdz" to "model/vnd.usdz+zip",
"ustar" to "application/x-ustar",
+ "utz" to "application/vnd.uiq.theme",
+ "uva" to "audio/vnd.dece.audio",
+ "uvd" to "application/vnd.dece.data",
+ "uvf" to "application/vnd.dece.data",
+ "uvg" to "image/vnd.dece.graphic",
+ "uvh" to "video/vnd.dece.hd",
+ "uvi" to "image/vnd.dece.graphic",
+ "uvm" to "video/vnd.dece.mobile",
+ "uvp" to "video/vnd.dece.pd",
+ "uvs" to "video/vnd.dece.sd",
+ "uvt" to "application/vnd.dece.ttml+xml",
+ "uvu" to "video/vnd.dece.mp4",
+ "uvv" to "video/vnd.dece.video",
+ "uvva" to "audio/vnd.dece.audio",
+ "uvvd" to "application/vnd.dece.data",
+ "uvvf" to "application/vnd.dece.data",
+ "uvvg" to "image/vnd.dece.graphic",
+ "uvvh" to "video/vnd.dece.hd",
+ "uvvi" to "image/vnd.dece.graphic",
+ "uvvm" to "video/vnd.dece.mobile",
+ "uvvp" to "video/vnd.dece.pd",
+ "uvvs" to "video/vnd.dece.sd",
+ "uvvt" to "application/vnd.dece.ttml+xml",
+ "uvvu" to "video/vnd.dece.mp4",
+ "uvvv" to "video/vnd.dece.video",
+ "uvvx" to "application/vnd.dece.unspecified",
+ "uvvz" to "application/vnd.dece.zip",
+ "uvx" to "application/vnd.dece.unspecified",
+ "uvz" to "application/vnd.dece.zip",
"val" to "chemical/x-ncbi-asn1-binary",
+ "vbk" to "audio/vnd.nortel.vbk",
+ "vbox" to "application/vnd.previewsystems.box",
"vcard" to "text/vcard",
"vcd" to "application/x-cdlink",
"vcf" to "text/x-vcard",
+ "vcg" to "application/vnd.groove-vcard",
+ "vcj" to "application/voucher-cms+json",
"vcs" to "text/x-vcalendar",
+ "vcx" to "application/vnd.vcx",
+ "vds" to "model/vnd.sap.vds",
+ "vew" to "application/vnd.lotus-approach",
+ "vfr" to "application/vnd.tml",
+ "viaframe" to "application/vnd.tml",
+ "vis" to "application/vnd.visionary",
+ "viv" to "video/vnd.vivo",
"vmd" to "chemical/x-vmd",
"vms" to "chemical/x-vamas-iso14976",
+ "vmt" to "application/vnd.valve.source.material",
"vor" to "application/vnd.stardivision.writer",
- "vrm" to "x-world/x-vrml",
- "vrml" to "x-world/x-vrml",
+ "vpm" to "multipart/voice-message",
+ "vrm" to "model/vrml",
+ "vrml" to "model/vrml",
+ "vsc" to "application/vnd.vidsoft.vidconference",
"vsd" to "application/vnd.visio",
+ "vsf" to "application/vnd.vsf",
"vss" to "application/vnd.visio",
"vst" to "application/vnd.visio",
"vsw" to "application/vnd.visio",
+ "vtf" to "image/vnd.valve.source.texture",
+ "vtnstd" to "application/vnd.veritone.aion+json",
+ "vtt" to "text/vtt",
+ "vtu" to "model/vnd.vtu",
+ "vwx" to "application/vnd.vectorworks",
+ "vxml" to "application/voicexml+xml",
"wad" to "application/x-doom",
+ "wadl" to "application/vnd.sun.wadl+xml",
+ "wafl" to "application/vnd.wasmflow.wafl",
"wasm" to "application/wasm",
"wav" to "audio/x-wav",
"wax" to "audio/x-ms-wax",
"wbmp" to "image/vnd.wap.wbmp",
+ "wbs" to "application/vnd.criticaltools.wbs+xml",
"wbxml" to "application/vnd.wap.wbxml",
+ "wcm" to "application/vnd.ms-works",
+ "wdb" to "application/vnd.ms-works",
"webarchive" to "application/x-webarchive",
"webarchivexml" to "application/x-webarchive-xml",
"webm" to "video/webm",
+ "webmanifest" to "application/manifest+json",
"webp" to "image/webp",
+ "wg" to "application/vnd.pmi.widget",
+ "wgsl" to "text/wgsl",
+ "wgt" to "application/widget",
+ "wif" to "application/watcherinfo+xml",
+ "win" to "model/vnd.gdl",
"wk" to "application/x-123",
+ "wk1" to "application/vnd.lotus-1-2-3",
+ "wk3" to "application/vnd.lotus-1-2-3",
+ "wk4" to "application/vnd.lotus-1-2-3",
+ "wks" to "application/vnd.ms-works",
+ "wlnk" to "application/link-format",
"wm" to "video/x-ms-wm",
"wma" to "audio/x-ms-wma",
+ "wmc" to "application/vnd.wmc",
"wmd" to "application/x-ms-wmd",
+ "wmf" to "image/wmf",
"wml" to "text/vnd.wap.wml",
"wmlc" to "application/vnd.wap.wmlc",
"wmls" to "text/vnd.wap.wmlscript",
@@ -590,24 +1499,58 @@ private val extensionToMimeTypeMap = mapOf(
"wmz" to "application/x-ms-wmz",
"woff" to "font/woff",
"woff2" to "font/woff2",
- "wp5" to "application/vnd.wordperfect5.1",
"wpd" to "application/vnd.wordperfect",
"wpl" to "application/vnd.ms-wpl",
+ "wps" to "application/vnd.ms-works",
+ "wqd" to "application/vnd.wqd",
"wrf" to "video/x-webex",
- "wrl" to "x-world/x-vrml",
- "wsc" to "text/scriptlet",
+ "wrl" to "model/vrml",
+ "wsc" to "application/vnd.wfa.wsc",
+ "wsdl" to "application/wsdl+xml",
+ "wspolicy" to "application/wspolicy+xml",
+ "wtb" to "application/vnd.webturbo",
+ "wv" to "application/vnd.wv.csp+wbxml",
"wvx" to "video/x-ms-wvx",
"wz" to "application/x-wingz",
"x3d" to "model/x3d+xml",
- "x3db" to "model/x3d+binary",
- "x3dv" to "model/x3d+vrml",
+ "x3db" to "model/x3d+fastinfoset",
+ "x3dv" to "model/x3d-vrml",
+ "x3dvz" to "model/x3d-vrml",
+ "x3dz" to "model/x3d+xml",
+ "x_b" to "model/vnd.parasolid.transmit.binary",
+ "x_t" to "model/vnd.parasolid.transmit.text",
+ "xar" to "application/vnd.xara",
+ "xav" to "application/xcap-att+xml",
+ "xbd" to "application/vnd.fujixerox.docuworks.binder",
"xbm" to "image/x-xbitmap",
- "xcf" to "application/x-xcf",
+ "xca" to "application/xcap-caps+xml",
+ "xcf" to "image/x-xcf",
"xcos" to "application/x-scilab-xcos",
+ "xcs" to "application/calendar+xml",
+ "xct" to "application/vnd.fujixerox.docuworks.container",
+ "xdd" to "application/bacnet-xdd+zip",
+ "xdf" to "application/xcap-diff+xml",
+ "xdm" to "application/vnd.syncml.dm+xml",
+ "xdp" to "application/vnd.adobe.xdp+xml",
+ "xdssc" to "application/dssc+xml",
+ "xdw" to "application/vnd.fujixerox.docuworks",
+ "xel" to "application/xcap-el+xml",
+ "xer" to "application/xcap-error+xml",
+ "xfd" to "application/vnd.xfdl",
+ "xfdf" to "application/xfdf",
+ "xfdl" to "application/vnd.xfdl",
+ "xhe" to "audio/usac",
"xht" to "application/xhtml+xml",
+ "xhtm" to "application/xhtml+xml",
"xhtml" to "application/xhtml+xml",
+ "xhvml" to "application/xv+xml",
+ "xif" to "image/vnd.xiff",
+ "xla" to "application/vnd.ms-excel",
"xlam" to "application/vnd.ms-excel.addin.macroEnabled.12",
- "xlb" to "application/vnd.ms-excel",
+ "xlc" to "application/vnd.ms-excel",
+ "xlf" to "application/xliff+xml",
+ "xlim" to "application/vnd.xmpie.xlim",
+ "xlm" to "application/vnd.ms-excel",
"xls" to "application/vnd.ms-excel",
"xlsb" to "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
"xlsm" to "application/vnd.ms-excel.sheet.macroEnabled.12",
@@ -615,22 +1558,57 @@ private val extensionToMimeTypeMap = mapOf(
"xlt" to "application/vnd.ms-excel",
"xltm" to "application/vnd.ms-excel.template.macroEnabled.12",
"xltx" to "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
+ "xlw" to "application/vnd.ms-excel",
"xmf" to "audio/midi",
"xml" to "text/xml",
+ "xmls" to "application/dskpp+xml",
+ "xmt_bin" to "model/vnd.parasolid.transmit.binary",
+ "xmt_txt" to "model/vnd.parasolid.transmit.text",
+ "xns" to "application/xcap-ns+xml",
+ "xo" to "application/vnd.olpc-sugar",
+ "xodp" to "application/vnd.collabio.xodocuments.presentation",
+ "xods" to "application/vnd.collabio.xodocuments.spreadsheet",
+ "xodt" to "application/vnd.collabio.xodocuments.document",
+ "xop" to "application/xop+xml",
+ "xotp" to "application/vnd.collabio.xodocuments.presentation-template",
+ "xots" to "application/vnd.collabio.xodocuments.spreadsheet-template",
+ "xott" to "application/vnd.collabio.xodocuments.document-template",
+ "xpak" to "application/vnd.gentoo.xpak",
"xpi" to "application/x-xpinstall",
"xpm" to "image/x-xpixmap",
- "xsd" to "application/xml",
+ "xpr" to "application/vnd.is-xpr",
+ "xps" to "application/vnd.ms-xpsdocument",
+ "xpw" to "application/vnd.intercon.formnet",
+ "xpx" to "application/vnd.intercon.formnet",
+ "xsf" to "application/prs.xsf+xml",
"xsl" to "application/xslt+xml",
"xslt" to "application/xslt+xml",
+ "xsm" to "application/vnd.syncml+xml",
"xspf" to "application/xspf+xml",
"xtel" to "chemical/x-xtel",
"xul" to "application/vnd.mozilla.xul+xml",
+ "xvm" to "application/xv+xml",
+ "xvml" to "application/xv+xml",
"xwd" to "image/x-xwindowdump",
"xyz" to "chemical/x-xyz",
+ "xyze" to "image/vnd.radiance",
"xz" to "application/x-xz",
+ "yaml" to "application/yaml",
+ "yang" to "application/yang",
+ "yin" to "application/yin+xml",
+ "yme" to "application/vnd.yaoweme",
+ "yml" to "application/yaml",
"yt" to "video/vnd.youtube.yt",
+ "zaz" to "application/vnd.zzazz.deck+xml",
+ "zfc" to "application/vnd.filmit.zfc",
+ "zfo" to "application/vnd.software602.filler.form-xml-zip",
"zip" to "application/zip",
+ "zir" to "application/vnd.zul",
+ "zirz" to "application/vnd.zul",
+ "zmm" to "application/vnd.HandHeld-Entertainment+xml",
"zmt" to "chemical/x-mopac-input",
+ "zone" to "text/dns",
+ "zst" to "application/zstd",
"~" to "application/x-trash"
)
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/compat/ServiceCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/ServiceCompat.kt
new file mode 100644
index 000000000..7a98fa0d0
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/compat/ServiceCompat.kt
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.compat
+
+import android.app.Service
+import androidx.core.app.ServiceCompat
+
+fun Service.stopForegroundCompat(flags: Int) {
+ ServiceCompat.stopForeground(this, flags)
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/compat/TextViewCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/TextViewCompat.kt
index 5ac7cd1e0..1785d5e58 100644
--- a/app/src/main/java/me/zhanghai/android/files/compat/TextViewCompat.kt
+++ b/app/src/main/java/me/zhanghai/android/files/compat/TextViewCompat.kt
@@ -5,9 +5,21 @@
package me.zhanghai.android.files.compat
+import android.os.Build
import android.widget.TextView
import androidx.annotation.StyleRes
import androidx.core.widget.TextViewCompat
+import me.zhanghai.android.files.util.lazyReflectedMethod
+
+private val isSingleLineMethod by lazyReflectedMethod(TextView::class.java, "isSingleLine")
+
+val TextView.isSingleLineCompat: Boolean
+ get() =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ isSingleLine
+ } else {
+ isSingleLineMethod.invoke(this) as Boolean
+ }
fun TextView.setTextAppearanceCompat(@StyleRes resId: Int) {
TextViewCompat.setTextAppearance(this, resId)
diff --git a/app/src/main/java/me/zhanghai/android/files/compat/TileServiceCompat.kt b/app/src/main/java/me/zhanghai/android/files/compat/TileServiceCompat.kt
new file mode 100644
index 000000000..98f6c4d00
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/compat/TileServiceCompat.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2024 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.compat
+
+import android.graphics.PixelFormat
+import android.os.Build
+import android.os.IBinder
+import android.service.quicksettings.TileService
+import android.view.View
+import android.view.WindowManager
+import androidx.annotation.RequiresApi
+import androidx.core.view.doOnPreDraw
+import me.zhanghai.android.files.hiddenapi.RestrictedHiddenApi
+import me.zhanghai.android.files.util.lazyReflectedField
+
+// Work around https://issuetracker.google.com/issues/299506164 on U which is fixed in V.
+fun TileService.doWithStartForegroundServiceAllowed(action: () -> Unit) {
+ if (Build.VERSION.SDK_INT != Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ action()
+ return
+ }
+ val windowManager = getSystemService(WindowManager::class.java)
+ val view = View(this)
+ val layoutParams =
+ WindowManager.LayoutParams().apply {
+ type = WindowManager_LayoutParams_TYPE_QS_DIALOG
+ format = PixelFormat.TRANSLUCENT
+ token = this@doWithStartForegroundServiceAllowed.token
+ }
+ windowManager.addView(view, layoutParams)
+ // We need to wait for WindowState.onSurfaceShownChanged(), basically when the first draw has
+ // finished and the surface is about to be shown to the user. However there's no good callback
+ // for that, while waiting for the second pre-draw seems to work.
+ view.doOnPreDraw {
+ view.post {
+ view.invalidate()
+ view.doOnPreDraw {
+ try {
+ action()
+ } finally {
+ windowManager.removeView(view)
+ }
+ }
+ }
+ }
+}
+
+private const val WindowManager_LayoutParams_TYPE_QS_DIALOG =
+ WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW + 35
+
+@delegate:RequiresApi(Build.VERSION_CODES.N)
+@get:RequiresApi(Build.VERSION_CODES.N)
+@RestrictedHiddenApi
+private val tokenField by lazyReflectedField(TileService::class.qualifiedName!!, "mToken")
+
+private val TileService.token: IBinder?
+ @RequiresApi(Build.VERSION_CODES.N)
+ get() = tokenField.get(this) as IBinder?
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/file/BasicFileAttributesExtensions.kt b/app/src/main/java/me/zhanghai/android/files/file/BasicFileAttributesExtensions.kt
index 2a31246b1..7626fc85c 100644
--- a/app/src/main/java/me/zhanghai/android/files/file/BasicFileAttributesExtensions.kt
+++ b/app/src/main/java/me/zhanghai/android/files/file/BasicFileAttributesExtensions.kt
@@ -5,8 +5,8 @@
package me.zhanghai.android.files.file
+import java.time.Instant
import java8.nio.file.attribute.BasicFileAttributes
-import org.threeten.bp.Instant
val BasicFileAttributes.fileSize: FileSize
get() = size().asFileSize()
diff --git a/app/src/main/java/me/zhanghai/android/files/file/DocumentTreeUri.kt b/app/src/main/java/me/zhanghai/android/files/file/DocumentTreeUri.kt
index a42a878f5..9937b9a81 100644
--- a/app/src/main/java/me/zhanghai/android/files/file/DocumentTreeUri.kt
+++ b/app/src/main/java/me/zhanghai/android/files/file/DocumentTreeUri.kt
@@ -11,10 +11,12 @@ import android.os.Parcelable
import android.os.storage.StorageVolume
import android.provider.DocumentsContract
import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.WriteWith
import me.zhanghai.android.files.app.contentResolver
import me.zhanghai.android.files.compat.DocumentsContractCompat
import me.zhanghai.android.files.compat.createOpenDocumentTreeIntentCompat
import me.zhanghai.android.files.storage.StorageVolumeListLiveData
+import me.zhanghai.android.files.util.StableUriParceler
import me.zhanghai.android.files.util.getParcelableExtraSafe
import me.zhanghai.android.files.util.releasePersistablePermission
import me.zhanghai.android.files.util.takePersistablePermission
@@ -22,7 +24,7 @@ import me.zhanghai.android.files.util.valueCompat
@Parcelize
@JvmInline
-value class DocumentTreeUri(val value: Uri) : Parcelable {
+value class DocumentTreeUri(val value: @WriteWith Uri) : Parcelable {
val documentId: String
get() = DocumentsContract.getTreeDocumentId(value)
diff --git a/app/src/main/java/me/zhanghai/android/files/file/DocumentUri.kt b/app/src/main/java/me/zhanghai/android/files/file/DocumentUri.kt
index 1300999fd..fac458337 100644
--- a/app/src/main/java/me/zhanghai/android/files/file/DocumentUri.kt
+++ b/app/src/main/java/me/zhanghai/android/files/file/DocumentUri.kt
@@ -6,12 +6,20 @@
package me.zhanghai.android.files.file
import android.net.Uri
+import android.os.Parcelable
import android.provider.DocumentsContract
+import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.WriteWith
import me.zhanghai.android.files.app.contentResolver
import me.zhanghai.android.files.compat.DocumentsContractCompat
+import me.zhanghai.android.files.util.StableUriParceler
+@Parcelize
@JvmInline
-value class DocumentUri(val value: Uri) {
+value class DocumentUri(val value: @WriteWith Uri) : Parcelable {
+ val treeDocumentId: String
+ get() = DocumentsContract.getTreeDocumentId(value)
+
val documentId: String
get() = DocumentsContract.getDocumentId(value)
}
@@ -50,6 +58,3 @@ val DocumentUri.displayName: String?
}
return null
}
-
-val DocumentUri.displayNameOrUri: String
- get() = displayName ?: value.toString()
diff --git a/app/src/main/java/me/zhanghai/android/files/file/DurationExtensions.kt b/app/src/main/java/me/zhanghai/android/files/file/DurationExtensions.kt
index b185c65d5..8e1b1f9ab 100644
--- a/app/src/main/java/me/zhanghai/android/files/file/DurationExtensions.kt
+++ b/app/src/main/java/me/zhanghai/android/files/file/DurationExtensions.kt
@@ -6,6 +6,6 @@
package me.zhanghai.android.files.file
import android.text.format.DateUtils
-import org.threeten.bp.Duration
+import java.time.Duration
fun Duration.format(): String = DateUtils.formatElapsedTime(seconds)
diff --git a/app/src/main/java/me/zhanghai/android/files/file/ExternalStorageUri.kt b/app/src/main/java/me/zhanghai/android/files/file/ExternalStorageUri.kt
new file mode 100644
index 000000000..2448615c8
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/file/ExternalStorageUri.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2024 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.file
+
+import android.net.Uri
+import android.os.Parcelable
+import android.provider.DocumentsContract
+import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.WriteWith
+import me.zhanghai.android.files.compat.DocumentsContractCompat
+import me.zhanghai.android.files.util.StableUriParceler
+import me.zhanghai.android.files.util.takeIfNotEmpty
+
+@Parcelize
+@JvmInline
+value class ExternalStorageUri(val value: @WriteWith Uri) : Parcelable {
+ constructor(
+ rootId: String,
+ path: String
+ ) : this(
+ DocumentsContract.buildDocumentUriUsingTree(
+ DocumentsContract.buildTreeDocumentUri(
+ DocumentsContractCompat.EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
+ rootId
+ ),
+ "$rootId:$path"
+ )
+ )
+
+ val rootId: String
+ get() = DocumentsContract.getTreeDocumentId(value)
+
+ val path: String
+ get() = DocumentsContract.getDocumentId(value).removePrefix("$rootId:")
+}
+
+fun Uri.asExternalStorageUriOrNull(): ExternalStorageUri? =
+ if (isExternalStorageUri) ExternalStorageUri(this) else null
+
+fun Uri.asExternalStorageUri(): ExternalStorageUri {
+ require(isExternalStorageUri)
+ return ExternalStorageUri(this)
+}
+
+/** @see DocumentsContractCompat.isDocumentUri */
+private val Uri.isExternalStorageUri: Boolean
+ get() =
+ DocumentsContractCompat.isDocumentUri(this) &&
+ authority == DocumentsContractCompat.EXTERNAL_STORAGE_PROVIDER_AUTHORITY &&
+ pathSegments.size == 4
+
+val ExternalStorageUri.displayName: String
+ get() = path.takeLastWhile { it != '/' }.takeIfNotEmpty() ?: "/"
diff --git a/app/src/main/java/me/zhanghai/android/files/file/InstantExtensions.kt b/app/src/main/java/me/zhanghai/android/files/file/InstantExtensions.kt
index 11efa2fbd..6a81e1a34 100644
--- a/app/src/main/java/me/zhanghai/android/files/file/InstantExtensions.kt
+++ b/app/src/main/java/me/zhanghai/android/files/file/InstantExtensions.kt
@@ -8,10 +8,10 @@ package me.zhanghai.android.files.file
import android.content.Context
import android.text.format.DateUtils
import android.text.format.Time
-import org.threeten.bp.Instant
-import org.threeten.bp.ZoneId
-import org.threeten.bp.format.DateTimeFormatter
-import org.threeten.bp.format.FormatStyle
+import java.time.Instant
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
/* @see com.android.documentsui.base.Shared#formatTime(Context, long) */
@Suppress("DEPRECATION")
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/file/MimeTypeConversionExtensions.kt b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeConversionExtensions.kt
index 6d0ab2f29..aeea2ce6d 100644
--- a/app/src/main/java/me/zhanghai/android/files/file/MimeTypeConversionExtensions.kt
+++ b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeConversionExtensions.kt
@@ -30,10 +30,8 @@ fun MimeType.Companion.guessFromExtension(extension: String): MimeType {
// @see /usr/share/mime/packages/freedesktop.org.xml
private val extensionToMimeTypeOverrideMap = mapOf(
// Fixes
- "cab" to "application/vnd.ms-cab-compressed", // Was "application/x-cab"
"csv" to "text/csv", // Was "text/comma-separated-values"
"sh" to "application/x-sh", // Was "text/x-sh"
- "otf" to "font/otf", // Was "font/ttf"
// Addition
"bz" to "application/x-bzip",
"bz2" to "application/x-bzip2",
@@ -41,15 +39,12 @@ private val extensionToMimeTypeOverrideMap = mapOf(
"lzma" to "application/x-lzma",
"p7b" to "application/x-pkcs7-certificates",
"spc" to "application/x-pkcs7-certificates", // Clashes with "chemical/x-galactic-spc"
- "p7c" to "application/pkcs7-mime",
- "p7s" to "application/pkcs7-signature",
"ts" to "application/typescript", // Clashes with "video/mp2ts"
"py3" to "text/x-python",
"py3x" to "text/x-python",
"pyx" to "text/x-python",
"wsgi" to "text/x-python",
- "yaml" to "text/x-yaml",
- "yml" to "text/x-yaml",
+ "yml" to "application/yaml",
"asm" to "text/x-asm",
"s" to "text/x-asm",
"cs" to "text/x-csharp",
@@ -79,6 +74,10 @@ private val specialPosixFileTypeToMimeTypeMap = mapOf(
PosixFileType.SOCKET to "inode/socket"
).mapValues { it.value.asMimeType() }
+val MimeType.extension: String?
+ // TODO: Add compat implementation as well.
+ get() = MimeTypeMap.getSingleton().getExtensionFromMimeType(value)
+
val MimeType.intentType: String
get() = intentMimeType.value
@@ -91,6 +90,7 @@ private val mimeTypeToIntentMimeTypeMap = listOf(
"application/javascript" to "text/javascript",
"application/json" to "text/json",
"application/typescript" to "text/typescript",
+ "application/yaml" to "text/x-yaml",
"application/x-sh" to "text/x-shellscript",
"application/x-shellscript" to "text/x-shellscript",
// Allows matching generic
diff --git a/app/src/main/java/me/zhanghai/android/files/file/MimeTypeIcon.kt b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeIcon.kt
index 5c891a548..0bfcce9e5 100644
--- a/app/src/main/java/me/zhanghai/android/files/file/MimeTypeIcon.kt
+++ b/app/src/main/java/me/zhanghai/android/files/file/MimeTypeIcon.kt
@@ -48,9 +48,10 @@ private val mimeTypeToIconMap = mapOf(
// Not in IANA list, but Mozilla and Wikipedia say so.
"application/java-archive" to MimeTypeIcon.ARCHIVE,
"application/mac-binhex40" to MimeTypeIcon.ARCHIVE,
- // Not in IANA list, but AOSP MimeUtils says so.
+ // Not in IANA list, but AOSP MimeUtils used to say so.
"application/rar" to MimeTypeIcon.ARCHIVE,
"application/zip" to MimeTypeIcon.ARCHIVE,
+ "application/zstd" to MimeTypeIcon.ARCHIVE,
"application/vnd.debian.binary-package" to MimeTypeIcon.ARCHIVE,
"application/vnd.ms-cab-compressed" to MimeTypeIcon.ARCHIVE,
"application/vnd.rar" to MimeTypeIcon.ARCHIVE,
@@ -97,6 +98,7 @@ private val mimeTypeToIconMap = mapOf(
"application/json" to MimeTypeIcon.CODE,
"application/typescript" to MimeTypeIcon.CODE,
"application/xml" to MimeTypeIcon.CODE,
+ "application/yaml" to MimeTypeIcon.CODE,
"application/x-csh" to MimeTypeIcon.CODE,
"application/x-ecmascript" to MimeTypeIcon.CODE,
"application/x-javascript" to MimeTypeIcon.CODE,
@@ -141,6 +143,7 @@ private val mimeTypeToIconMap = mapOf(
"inode/directory" to MimeTypeIcon.DIRECTORY,
MimeType.DIRECTORY.value to MimeTypeIcon.DIRECTORY,
"application/rtf" to MimeTypeIcon.DOCUMENT,
+ "application/vnd.kde.kword" to MimeTypeIcon.DOCUMENT,
"application/vnd.oasis.opendocument.text" to MimeTypeIcon.DOCUMENT,
"application/vnd.oasis.opendocument.text-master" to MimeTypeIcon.DOCUMENT,
"application/vnd.oasis.opendocument.text-template" to MimeTypeIcon.DOCUMENT,
@@ -155,6 +158,9 @@ private val mimeTypeToIconMap = mapOf(
"text/rtf" to MimeTypeIcon.DOCUMENT,
"application/epub+zip" to MimeTypeIcon.EBOOK,
"application/vnd.amazon.ebook" to MimeTypeIcon.EBOOK,
+ "application/vnd.amazon.mobi8-ebook" to MimeTypeIcon.EBOOK,
+ "application/vnd.comicbook-rar" to MimeTypeIcon.EBOOK,
+ "application/vnd.comicbook+zip" to MimeTypeIcon.EBOOK,
"application/x-cbr" to MimeTypeIcon.EBOOK,
"application/x-cbz" to MimeTypeIcon.EBOOK,
"application/x-ibooks+zip" to MimeTypeIcon.EBOOK,
@@ -179,18 +185,21 @@ private val mimeTypeToIconMap = mapOf(
"application/vnd.sun.xml.draw.template" to MimeTypeIcon.IMAGE,
"application/vnd.visio" to MimeTypeIcon.IMAGE,
"application/pdf" to MimeTypeIcon.PDF,
+ "application/vnd.kde.kpresenter" to MimeTypeIcon.PRESENTATION,
"application/vnd.oasis.opendocument.presentation" to MimeTypeIcon.PRESENTATION,
"application/vnd.oasis.opendocument.presentation-template" to MimeTypeIcon.PRESENTATION,
"application/vnd.stardivision.impress" to MimeTypeIcon.PRESENTATION,
"application/vnd.sun.xml.impress" to MimeTypeIcon.PRESENTATION,
"application/vnd.sun.xml.impress.template" to MimeTypeIcon.PRESENTATION,
"application/x-kpresenter" to MimeTypeIcon.PRESENTATION,
+ "application/vnd.kde.kspread" to MimeTypeIcon.SPREADSHEET,
"application/vnd.oasis.opendocument.spreadsheet" to MimeTypeIcon.SPREADSHEET,
"application/vnd.oasis.opendocument.spreadsheet-template" to MimeTypeIcon.SPREADSHEET,
"application/vnd.stardivision.calc" to MimeTypeIcon.SPREADSHEET,
"application/vnd.sun.xml.calc" to MimeTypeIcon.SPREADSHEET,
"application/vnd.sun.xml.calc.template" to MimeTypeIcon.SPREADSHEET,
"application/x-kspread" to MimeTypeIcon.SPREADSHEET,
+ "application/vnd.adobe.flash.movie" to MimeTypeIcon.VIDEO,
"application/x-quicktimeplayer" to MimeTypeIcon.VIDEO,
"application/x-shockwave-flash" to MimeTypeIcon.VIDEO,
"application/msword" to MimeTypeIcon.WORD,
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..90e95f247 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
@@ -18,26 +16,29 @@ private val supportedArchiveMimeTypes = mutableListOf(
"application/java-archive",
"application/rar",
"application/zip",
+ "application/zstd",
"application/vnd.android.package-archive",
"application/vnd.debian.binary-package",
+ "application/vnd.ms-cab-compressed",
+ "application/vnd.rar",
+ "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/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 2d648c4f5..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
@@ -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
@@ -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
@@ -138,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 */
@@ -151,37 +155,51 @@ class FileJobConflictDialogFragment : AppCompatDialogFragment() {
descriptionText: TextView
) {
val path = file.path
- iconImage.setImageResource(file.mimeType.iconRes)
- iconImage.isVisible = true
- thumbnailImage.clear()
- thumbnailImage.setImageDrawable(null)
+ iconImage.apply {
+ isVisible = true
+ setImageResource(file.mimeType.iconRes)
+ }
val attributes = file.attributes
- if (file.supportsThumbnail) {
- thumbnailImage.loadAny(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.clear()
- appIconBadgeImage.setImageDrawable(null)
- val appDirectoryPackageName = file.appDirectoryPackageName
- val hasAppIconBadge = appDirectoryPackageName != null
- appIconBadgeImage.isVisible = hasAppIconBadge
- if (hasAppIconBadge) {
- appIconBadgeImage.loadAny(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)
@@ -235,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)
}
}
@@ -334,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/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/filejob/FileJobService.kt b/app/src/main/java/me/zhanghai/android/files/filejob/FileJobService.kt
index c5b7b2e7e..faa013ac4 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,15 @@ 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
+import me.zhanghai.android.files.compat.removeFirstCompat
class FileJobService : Service() {
+ private lateinit var wakeWifiLock: WakeWifiLock
+
internal lateinit var notificationManager: ForegroundNotificationManager
private set
@@ -31,35 +35,41 @@ class FileJobService : Service() {
override fun onCreate() {
super.onCreate()
+ wakeWifiLock = WakeWifiLock(FileJobService::class.java.simpleName)
notificationManager = ForegroundNotificationManager(this)
instance = this
while (pendingJobs.isNotEmpty()) {
- startJob(pendingJobs.removeFirst())
+ startJob(pendingJobs.removeFirstCompat())
}
}
+ 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 +82,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
@@ -98,11 +115,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) {
@@ -137,6 +155,10 @@ class FileJobService : Service() {
startJob(RestoreFileSeLinuxContextJob(path, recursive), context)
}
+ fun save(source: Path, target: Path, context: Context) {
+ startJob(SaveFileJob(source, target), context)
+ }
+
fun setGroup(path: Path, group: PosixGroup, recursive: Boolean, context: Context) {
startJob(SetFileGroupJob(path, group, recursive), 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..4213b2374 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
@@ -3,13 +3,14 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.filejob;
+package me.zhanghai.android.files.filejob
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
+import android.widget.Toast
import androidx.annotation.AnyRes
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
@@ -30,6 +31,7 @@ import kotlinx.coroutines.runBlocking
import me.zhanghai.android.files.R
import me.zhanghai.android.files.app.BackgroundActivityStarter
import me.zhanghai.android.files.app.mainExecutor
+import me.zhanghai.android.files.compat.mainExecutorCompat
import me.zhanghai.android.files.file.FileItem
import me.zhanghai.android.files.file.MimeType
import me.zhanghai.android.files.file.asFileSize
@@ -51,6 +53,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
@@ -84,6 +87,7 @@ import me.zhanghai.android.files.util.createViewIntent
import me.zhanghai.android.files.util.extraPath
import me.zhanghai.android.files.util.getQuantityString
import me.zhanghai.android.files.util.putArgs
+import me.zhanghai.android.files.util.showToast
import me.zhanghai.android.files.util.toEnumSet
import me.zhanghai.android.files.util.withChooser
import java.io.ByteArrayInputStream
@@ -152,6 +156,18 @@ private const val PROGRESS_INTERVAL_MILLIS = 200L
private const val NOTIFICATION_INTERVAL_MILLIS = 500L
+private fun FileJob.showToast(textRes: Int, duration: Int = Toast.LENGTH_SHORT) {
+ service.mainExecutorCompat.execute {
+ service.showToast(textRes, duration)
+ }
+}
+
+private fun FileJob.showToast(text: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
+ service.mainExecutorCompat.execute {
+ service.showToast(text, duration)
+ }
+}
+
private fun FileJob.getFileName(path: Path): String =
if (path.isAbsolute && path.nameCount == 0) {
path.fileSystem.separator
@@ -467,8 +483,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 +508,17 @@ private fun FileJob.showActionDialog(
positiveButtonText: CharSequence?,
negativeButtonText: CharSequence?,
neutralButtonText: CharSequence?
-): ActionResult =
+): ErrorResult =
try {
- runBlocking {
+ 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 +541,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
)
@@ -521,7 +553,7 @@ private fun FileJob.showConflictDialog(
type: CopyMoveType
): ConflictResult =
try {
- runBlocking {
+ runBlocking {
suspendCoroutine { continuation ->
BackgroundActivityStarter.startActivity(
FileJobConflictDialogActivity::class.createIntent().putArgs(
@@ -530,8 +562,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
)
}
@@ -582,8 +614,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 +627,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 {
@@ -673,7 +706,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),
@@ -686,7 +719,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)
}
}
@@ -866,7 +900,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) {
@@ -878,7 +912,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()
@@ -890,11 +931,12 @@ private fun FileJob.create(path: Path, createDirectory: Boolean) {
null
)
when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- FileJobAction.NEGATIVE, FileJobAction.CANCELED -> throw InterruptedIOException()
+ FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED ->
+ throw InterruptedIOException()
else -> throw AssertionError(result.action)
}
}
@@ -953,7 +995,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()
@@ -972,7 +1014,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()
@@ -984,11 +1033,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
+ continue
}
- FileJobAction.NEGATIVE -> {
+ FileJobErrorAction.NEGATIVE -> {
if (result.isAll) {
actionAllInfo.skipDeleteError = true
}
@@ -998,14 +1047,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)
}
}
@@ -1152,7 +1201,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,
@@ -1168,7 +1217,7 @@ private fun FileJob.copyOrMove(
null
)
return when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
if (result.isAll) {
actionAllInfo.skipCopyMoveIntoItself = true
}
@@ -1176,12 +1225,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)
}
}
@@ -1192,7 +1241,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,
@@ -1208,7 +1257,7 @@ private fun FileJob.copyOrMove(
null
)
return when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
if (result.isAll) {
actionAllInfo.skipCopyMoveOverItself = true
}
@@ -1216,19 +1265,19 @@ 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)
}
}
var target = target
var replaceExisting = false
var retry: Boolean
- loop@ do {
+ do {
retry = false
val options = mutableListOf().apply {
this += LinkOption.NOFOLLOW_LINKS
@@ -1293,13 +1342,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) {
@@ -1336,7 +1385,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,
@@ -1358,11 +1414,11 @@ private fun FileJob.copyOrMove(
getString(android.R.string.cancel)
)
return when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- FileJobAction.NEGATIVE -> {
+ FileJobErrorAction.NEGATIVE -> {
if (result.isAll) {
actionAllInfo.skipCopyMoveError = true
}
@@ -1370,12 +1426,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)
@@ -1471,7 +1527,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
@@ -1489,7 +1548,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)
@@ -1497,7 +1556,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),
@@ -1510,11 +1576,12 @@ private fun FileJob.rename(path: Path, newPath: Path) {
null
)
when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- FileJobAction.NEGATIVE, FileJobAction.CANCELED -> throw InterruptedIOException()
+ FileJobErrorAction.NEGATIVE, FileJobErrorAction.CANCELED ->
+ throw InterruptedIOException()
else -> throw AssertionError(result.action)
}
}
@@ -1573,7 +1640,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)
@@ -1589,7 +1656,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,
@@ -1602,11 +1676,11 @@ private fun FileJob.restoreSeLinuxContext(
getString(android.R.string.cancel)
)
when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- FileJobAction.NEGATIVE -> {
+ FileJobErrorAction.NEGATIVE -> {
if (result.isAll) {
actionAllInfo.skipRestoreSeLinuxContextError = true
}
@@ -1614,12 +1688,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)
}
}
@@ -1637,6 +1711,27 @@ private fun FileJob.postRestoreSeLinuxContextNotification(
)
}
+class SaveFileJob(private val source: Path, private val target: Path) : FileJob() {
+ override fun run() {
+ save(source, target)
+ }
+}
+
+@Throws(IOException::class)
+private fun FileJob.save(source: Path, target: Path) {
+ val scanInfo = scan(source, R.plurals.file_job_copy_scan_notification_title_format)
+ val targetParent = target.parent
+ val transferInfo = TransferInfo(scanInfo, targetParent)
+ val actionAllInfo = ActionAllInfo(replace = true)
+ val copied = copy(source, target, false, transferInfo, actionAllInfo)
+ if (!copied) {
+ return
+ }
+ showToast(
+ getString(R.string.save_as_success_format, getFileName(target), getFileName(targetParent))
+ )
+}
+
class SetFileGroupJob(
private val path: Path,
private val group: PosixGroup,
@@ -1690,7 +1785,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)
@@ -1706,7 +1801,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),
@@ -1719,11 +1821,11 @@ private fun FileJob.setGroup(
getString(android.R.string.cancel)
)
when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- FileJobAction.NEGATIVE -> {
+ FileJobErrorAction.NEGATIVE -> {
if (result.isAll) {
actionAllInfo.skipSetGroupError = true
}
@@ -1731,12 +1833,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)
}
}
@@ -1829,7 +1931,7 @@ private fun FileJob.setMode(
actionAllInfo: ActionAllInfo
) {
var retry: Boolean
- loop@ do {
+ do {
retry = false
try {
// This will always follow symbolic links.
@@ -1845,7 +1947,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(),
@@ -1858,11 +1967,11 @@ private fun FileJob.setMode(
getString(android.R.string.cancel)
)
when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- FileJobAction.NEGATIVE -> {
+ FileJobErrorAction.NEGATIVE -> {
if (result.isAll) {
actionAllInfo.skipSetModeError = true
}
@@ -1870,12 +1979,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)
}
}
@@ -1942,7 +2051,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)
@@ -1958,7 +2067,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),
@@ -1971,11 +2087,11 @@ private fun FileJob.setOwner(
getString(android.R.string.cancel)
)
when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- FileJobAction.NEGATIVE -> {
+ FileJobErrorAction.NEGATIVE -> {
if (result.isAll) {
actionAllInfo.skipSetOwnerError = true
}
@@ -1983,12 +2099,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)
}
}
@@ -2063,7 +2179,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)
@@ -2079,7 +2195,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)
),
@@ -2094,11 +2217,11 @@ private fun FileJob.setSeLinuxContext(
getString(android.R.string.cancel)
)
when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- FileJobAction.NEGATIVE -> {
+ FileJobErrorAction.NEGATIVE -> {
if (result.isAll) {
actionAllInfo.skipSetSeLinuxContextError = true
}
@@ -2106,12 +2229,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)
}
}
@@ -2148,7 +2271,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 {
@@ -2163,7 +2286,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()
@@ -2175,12 +2305,12 @@ private fun FileJob.write(file: Path, content: ByteArray): Boolean {
null
)
return when (result.action) {
- FileJobAction.POSITIVE -> {
+ FileJobErrorAction.POSITIVE -> {
retry = true
- continue@loop
+ continue
}
- 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/filelist/BreadcrumbLayout.kt b/app/src/main/java/me/zhanghai/android/files/filelist/BreadcrumbLayout.kt
index e793f1dd8..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
@@ -36,8 +36,9 @@ class BreadcrumbLayout : HorizontalScrollView {
context.getColorByAttr(android.R.attr.textColorSecondary)
)
)
- private val popupContext =
- context.withTheme(context.getResourceIdByAttr(R.attr.actionBarPopupTheme))
+ private val popupContext = context.withTheme(
+ context.getResourceIdByAttr(androidx.appcompat.R.attr.actionBarPopupTheme)
+ )
private val itemsLayout: LinearLayout
@@ -87,15 +88,18 @@ class BreadcrumbLayout : HorizontalScrollView {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
- var heightMeasureSpec = heightMeasureSpec
- if (heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST) {
- var height = tabLayoutHeight
- if (heightMode == MeasureSpec.AT_MOST) {
- height = height.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
+ val newHeightMeasureSpec = if (heightMode != MeasureSpec.EXACTLY) {
+ val maximumHeight = if (heightMode == MeasureSpec.AT_MOST) {
+ MeasureSpec.getSize(heightMeasureSpec)
+ } else {
+ Int.MAX_VALUE
}
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
+ val height = tabLayoutHeight.coerceAtMost(maximumHeight)
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
+ } else {
+ heightMeasureSpec
}
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ super.onMeasure(widthMeasureSpec, newHeightMeasureSpec)
}
override fun requestLayout() {
@@ -150,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..
+ * 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 kotlinx.parcelize.Parcelize
+import me.zhanghai.android.files.R
+import me.zhanghai.android.files.file.FileItem
+import me.zhanghai.android.files.util.ParcelableArgs
+import me.zhanghai.android.files.util.args
+import me.zhanghai.android.files.util.putArgs
+import me.zhanghai.android.files.util.show
+
+class ConfirmReplaceFileDialogFragment : AppCompatDialogFragment() {
+ private val args by args()
+
+ private val listener: Listener
+ get() = requireParentFragment() as Listener
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val file = args.file
+ return MaterialAlertDialogBuilder(requireContext(), theme)
+ .setMessage(getString(R.string.file_replace_message_format, file.name))
+ .setPositiveButton(android.R.string.ok) { _, _ -> listener.replaceFile(file) }
+ .setNegativeButton(android.R.string.cancel, null)
+ .create()
+ }
+
+ companion object {
+ fun show(file: FileItem, fragment: Fragment) {
+ ConfirmReplaceFileDialogFragment().putArgs(Args(file)).show(fragment)
+ }
+ }
+
+ @Parcelize
+ class Args(val file: FileItem) : ParcelableArgs
+
+ interface Listener {
+ fun replaceFile(file: FileItem)
+ }
+}
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..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,8 +25,8 @@ 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.files.util.takeIfNotEmpty
+import me.zhanghai.android.libarchive.Archive
class CreateArchiveDialogFragment : FileNameDialogFragment() {
private val args by args()
@@ -51,6 +53,8 @@ class CreateArchiveDialogFragment : FileNameDialogFragment() {
}
name?.let { binding.nameEdit.setTextWithSelection(it) }
}
+ binding.typeGroup.setOnCheckedChangeListener { _, _ -> updatePasswordLayoutVisibility() }
+ updatePasswordLayoutVisibility()
return dialog
}
@@ -62,34 +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 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
- }
- else -> throw AssertionError(typeId)
+ 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(checkedId)
+ }
+ val password = if (isPasswordSupported) {
+ binding.passwordEdit.text!!.toString().takeIfNotEmpty()
+ } else {
+ null
}
- listener.archive(args.files, name, archiveType, compressorType)
+ listener.archive(args.files, name, format, filter, password)
}
companion object {
@@ -105,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 {
@@ -113,13 +124,14 @@ 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
)
}
}
}
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/FileItemExtensions.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileItemExtensions.kt
index 76b94dfba..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
@@ -21,12 +21,12 @@ 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
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
@@ -61,17 +61,17 @@ 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
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
diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListActivity.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListActivity.kt
index 82315a029..982342861 100644
--- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListActivity.kt
+++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListActivity.kt
@@ -8,6 +8,7 @@ package me.zhanghai.android.files.filelist
import android.content.Context
import android.content.Intent
import android.os.Bundle
+import android.view.KeyEvent
import android.view.View
import androidx.activity.result.contract.ActivityResultContract
import androidx.fragment.app.commit
@@ -35,11 +36,11 @@ class FileListActivity : AppActivity() {
}
}
- override fun onBackPressed() {
- if (fragment.onBackPressed()) {
- return
+ override fun onKeyShortcut(keyCode: Int, event: KeyEvent): Boolean {
+ if (fragment.onKeyShortcut(keyCode, event)) {
+ return true
}
- super.onBackPressed()
+ return super.onKeyUp(keyCode, event)
}
companion object {
@@ -49,22 +50,41 @@ class FileListActivity : AppActivity() {
.apply { extraPath = path }
}
- class PickDirectoryContract : ActivityResultContract() {
- override fun createIntent(context: Context, input: Path?): Intent =
+ class OpenFileContract : ActivityResultContract, Path?>() {
+ override fun createIntent(context: Context, input: List): Intent =
FileListActivity::class.createIntent()
- .setAction(Intent.ACTION_OPEN_DOCUMENT_TREE)
- .apply { input?.let { extraPath = it } }
+ .setAction(Intent.ACTION_OPEN_DOCUMENT)
+ .setType(MimeType.ANY.value)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .putExtra(Intent.EXTRA_MIME_TYPES, input.map { it.value }.toTypedArray())
override fun parseResult(resultCode: Int, intent: Intent?): Path? =
if (resultCode == RESULT_OK) intent?.extraPath else null
}
- class PickFileContract : ActivityResultContract, Path?>() {
- override fun createIntent(context: Context, input: List): Intent =
+ class CreateFileContract : ActivityResultContract, Path?>() {
+ override fun createIntent(
+ context: Context,
+ input: Triple
+ ): Intent =
FileListActivity::class.createIntent()
- .setAction(Intent.ACTION_OPEN_DOCUMENT)
- .setType(MimeType.ANY.value)
- .putExtra(Intent.EXTRA_MIME_TYPES, input.map { it.value }.toTypedArray())
+ .setAction(Intent.ACTION_CREATE_DOCUMENT)
+ .setType(input.first.value)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .apply {
+ input.second?.let { putExtra(Intent.EXTRA_TITLE, it) }
+ input.third?.let { extraPath = it }
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): Path? =
+ if (resultCode == RESULT_OK) intent?.extraPath else null
+ }
+
+ class OpenDirectoryContract : ActivityResultContract() {
+ override fun createIntent(context: Context, input: Path?): Intent =
+ FileListActivity::class.createIntent()
+ .setAction(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ .apply { input?.let { extraPath = it } }
override fun parseResult(resultCode: Int, intent: Intent?): Path? =
if (resultCode == RESULT_OK) intent?.extraPath else null
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..0d300aed3 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
@@ -6,26 +6,38 @@
package me.zhanghai.android.files.filelist
import android.text.TextUtils
+import android.view.View
import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.TextView
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
import me.zhanghai.android.files.coil.AppIconPackageName
-import me.zhanghai.android.files.databinding.FileItemBinding
+import me.zhanghai.android.files.compat.foregroundCompat
+import me.zhanghai.android.files.compat.getDrawableCompat
+import me.zhanghai.android.files.compat.isSingleLineCompat
+import me.zhanghai.android.files.databinding.FileItemGridBinding
+import me.zhanghai.android.files.databinding.FileItemListBinding
import me.zhanghai.android.files.file.FileItem
import me.zhanghai.android.files.file.fileSize
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
import me.zhanghai.android.files.ui.CheckableItemBackground
+import me.zhanghai.android.files.util.isMaterial3Theme
import me.zhanghai.android.files.util.layoutInflater
import me.zhanghai.android.files.util.valueCompat
import java.util.Locale
@@ -35,13 +47,24 @@ class FileListAdapter(
) : AnimatedListAdapter(CALLBACK), PopupTextProvider {
private var isSearching = false
- private lateinit var _comparator: Comparator
- var comparator: Comparator
- get() = _comparator
+ private lateinit var _viewType: FileViewType
+ var viewType: FileViewType
+ get() = _viewType
set(value) {
- _comparator = value
+ _viewType = value
if (!isSearching) {
- super.replace(list.sortedWith(value), true)
+ super.replace(list, true)
+ }
+ }
+
+ private lateinit var _sortOptions: FileSortOptions
+ var sortOptions: FileSortOptions
+ get() = _sortOptions
+ set(value) {
+ _sortOptions = value
+ if (!isSearching) {
+ val sortedList = list.sortedWith(value.createComparator())
+ super.replace(sortedList, true)
rebuildFilePositionMap()
}
}
@@ -100,7 +123,7 @@ class FileListAdapter(
fun selectAllFiles() {
val files = fileItemSetOf()
- for (index in 0 until itemCount) {
+ for (index in 0..
+ !file.attributes.isDirectory &&
+ pickOptions.mimeTypes.any { it.match(file.mimeType) }
+ PickOptions.Mode.OPEN_DIRECTORY -> file.attributes.isDirectory
}
}
@@ -132,28 +156,55 @@ 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()
}
private fun rebuildFilePositionMap() {
filePositionMap.clear()
- for (index in 0 until itemCount) {
+ for (index in 0.. ViewHolder(FileItemListBinding.inflate(inflater, parent, false))
+ FileViewType.GRID -> ViewHolder(FileItemGridBinding.inflate(inflater, parent, false))
+ }
+ return holder.apply {
+ itemLayout.apply {
+ val context = context
+ val isMaterial3Theme = context.isMaterial3Theme
+ if (viewType == FileViewType.GRID && isMaterial3Theme) {
+ foregroundCompat =
+ context.getDrawableCompat(R.drawable.file_item_grid_foreground_material3)
+ }
+ background = if (viewType == FileViewType.GRID && isMaterial3Theme) {
+ CheckableItemBackground.create(4f, 12f, context)
+ } else {
+ CheckableItemBackground.create(0f, 0f, context)
+ }
+ }
+ thumbnailOutlineView?.apply {
+ val context = context
+ if (context.isMaterial3Theme) {
+ background = context.getDrawableCompat(
+ R.drawable.file_item_grid_thumbnail_outline_material3
+ )
+ }
+ }
+ popupMenu = PopupMenu(menuButton.context, menuButton)
.apply { inflate(R.menu.file_item) }
- binding.menuButton.setOnClickListener { popupMenu.show() }
+ menuButton.setOnClickListener { popupMenu.show() }
}
+ }
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
throw UnsupportedOperationException()
@@ -161,11 +212,10 @@ class FileListAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List) {
val file = getItem(position)
- val binding = holder.binding
val isDirectory = file.attributes.isDirectory
- val enabled = isFileSelectable(file) || isDirectory
- binding.itemLayout.isEnabled = enabled
- binding.menuButton.isEnabled = enabled
+ val isEnabled = isFileSelectable(file) || isDirectory
+ holder.itemLayout.isEnabled = isEnabled
+ holder.menuButton.isEnabled = isEnabled
val menu = holder.popupMenu.menu
val path = file.path
val hasPickOptions = pickOptions != null
@@ -173,69 +223,104 @@ class FileListAdapter(
menu.findItem(R.id.action_cut).isVisible = !hasPickOptions && !isReadOnly
menu.findItem(R.id.action_copy).isVisible = !hasPickOptions
val checked = file in selectedFiles
- binding.itemLayout.isChecked = checked
- val nameEllipsize = nameEllipsize
- binding.nameText.ellipsize = nameEllipsize
- binding.nameText.isSelected = nameEllipsize == TextUtils.TruncateAt.MARQUEE
+ holder.itemLayout.isChecked = checked
+ holder.nameText.apply {
+ if (isSingleLineCompat) {
+ val nameEllipsize = nameEllipsize
+ ellipsize = nameEllipsize
+ isSelected = nameEllipsize == TextUtils.TruncateAt.MARQUEE
+ }
+ }
if (payloads.isNotEmpty()) {
return
}
bindViewHolderAnimation(holder)
- binding.itemLayout.setOnClickListener {
- if (selectedFiles.isEmpty()) {
- listener.openFile(file)
- } else {
- selectFile(file)
+ holder.itemLayout.apply {
+ setOnClickListener {
+ if (selectedFiles.isEmpty()) {
+ listener.openFile(file)
+ } else {
+ selectFile(file)
+ }
}
- }
- binding.itemLayout.setOnLongClickListener {
- if (selectedFiles.isEmpty()) {
- selectFile(file)
- } else {
- listener.openFile(file)
+ setOnLongClickListener {
+ if (selectedFiles.isEmpty()) {
+ selectFile(file)
+ } else {
+ listener.openFile(file)
+ }
+ true
}
- true
}
- binding.iconLayout.setOnClickListener { selectFile(file) }
- binding.iconImage.setImageResource(file.mimeType.iconRes)
- binding.iconImage.isVisible = true
- binding.thumbnailImage.clear()
- binding.thumbnailImage.setImageDrawable(null)
+ holder.iconLayout.setOnClickListener { selectFile(file) }
+ val iconRes = file.mimeType.iconRes
+ holder.iconImage.apply {
+ isVisible = true
+ setImageResource(iconRes)
+ }
+ holder.directoryThumbnailImage?.isVisible = isDirectory
+ holder.thumbnailOutlineView?.isVisible = !isDirectory
val supportsThumbnail = file.supportsThumbnail
- binding.thumbnailImage.isVisible = supportsThumbnail
+ val shouldLoadThumbnailIcon = supportsThumbnail && holder.thumbnailIconImage != null &&
+ file.mimeType.isApk
val attributes = file.attributes
- if (supportsThumbnail) {
- binding.thumbnailImage.loadAny(path to attributes) {
- listener { _, _ -> binding.iconImage.isVisible = false }
+ holder.thumbnailIconImage?.apply {
+ dispose()
+ isVisible = !isDirectory
+ setImageResource(iconRes)
+ if (shouldLoadThumbnailIcon) {
+ load(path to attributes)
}
}
- binding.appIconBadgeImage.clear()
- binding.appIconBadgeImage.setImageDrawable(null)
- val appDirectoryPackageName = file.appDirectoryPackageName
- val hasAppIconBadge = appDirectoryPackageName != null
- binding.appIconBadgeImage.isVisible = hasAppIconBadge
- if (hasAppIconBadge) {
- binding.appIconBadgeImage.loadAny(AppIconPackageName(appDirectoryPackageName!!))
+ holder.thumbnailImage.apply {
+ dispose()
+ setImageDrawable(null)
+ val shouldLoadThumbnail = supportsThumbnail && !shouldLoadThumbnailIcon
+ isVisible = shouldLoadThumbnail
+ if (shouldLoadThumbnail) {
+ load(path to attributes) {
+ listener { _, _ ->
+ val iconImage = holder.thumbnailIconImage ?: holder.iconImage
+ iconImage.isVisible = false
+ }
+ }
+ }
}
- val badgeIconRes = if (file.attributesNoFollowLinks.isSymbolicLink) {
- if (file.isSymbolicLinkBroken) {
- R.drawable.error_badge_icon_18dp
- } else {
- R.drawable.symbolic_link_badge_icon_18dp
+ holder.appIconBadgeImage.apply {
+ dispose()
+ setImageDrawable(null)
+ val appDirectoryPackageName = file.appDirectoryPackageName
+ val hasAppIconBadge = appDirectoryPackageName != null
+ isVisible = hasAppIconBadge
+ if (hasAppIconBadge) {
+ load(AppIconPackageName(appDirectoryPackageName!!))
}
- } else {
- null
}
- val hasBadge = badgeIconRes != null
- binding.badgeImage.isVisible = hasBadge
- if (hasBadge) {
- binding.badgeImage.setImageResource(badgeIconRes!!)
+ holder.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 {
+ null
+ }
+ val hasBadge = badgeIconRes != null
+ isVisible = hasBadge
+ if (hasBadge) {
+ setImageResource(badgeIconRes!!)
+ } else {
+ setImageDrawable(null)
+ }
}
- binding.nameText.text = file.name
- binding.descriptionText.text = if (isDirectory) {
+ holder.nameText.text = file.name
+ holder.descriptionText?.text = if (isDirectory) {
null
} else {
- val context = binding.descriptionText.context
+ val context = holder.descriptionText!!.context
val lastModificationTime = attributes.lastModifiedTime().toInstant()
.formatShort(context)
val size = attributes.fileSize.formatHumanReadable(context)
@@ -305,9 +390,15 @@ class FileListAdapter(
}
}
- override fun getPopupText(position: Int): String {
+ 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
@@ -325,7 +416,53 @@ class FileListAdapter(
}
}
- class ViewHolder(val binding: FileItemBinding) : RecyclerView.ViewHolder(binding.root) {
+ class ViewHolder private constructor(
+ root: View,
+ val itemLayout: CheckableForegroundLinearLayout,
+ val iconLayout: View,
+ val iconImage: ImageView,
+ val directoryThumbnailImage: ImageView?,
+ val thumbnailOutlineView: View?,
+ val thumbnailIconImage: ImageView?,
+ val thumbnailImage: ImageView,
+ val appIconBadgeImage: ImageView,
+ val badgeImage: ImageView,
+ val nameText: TextView,
+ val descriptionText: TextView?,
+ val menuButton: ImageButton
+ ) : RecyclerView.ViewHolder(root) {
+ constructor(binding: FileItemListBinding) : this(
+ binding.root,
+ binding.itemLayout,
+ binding.iconLayout,
+ binding.iconImage,
+ null,
+ null,
+ null,
+ binding.thumbnailImage,
+ binding.appIconBadgeImage,
+ binding.badgeImage,
+ binding.nameText,
+ binding.descriptionText,
+ binding.menuButton
+ )
+
+ constructor(binding: FileItemGridBinding) : this(
+ binding.root,
+ binding.itemLayout,
+ binding.iconLayout,
+ binding.iconImage,
+ binding.directoryThumbnailImage,
+ binding.thumbnailOutlineView,
+ binding.thumbnailIconImage,
+ binding.thumbnailImage,
+ binding.appIconBadgeImage,
+ binding.badgeImage,
+ binding.nameText,
+ null,
+ binding.menuButton
+ )
+
lateinit var popupMenu: PopupMenu
}
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 72c7b06ac..5c02a1c79 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
@@ -17,14 +17,18 @@ import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
+import android.view.KeyCharacterMap
+import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import android.widget.EditText
import android.widget.ProgressBar
import android.widget.TextView
+import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
@@ -35,6 +39,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.view.GravityCompat
+import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
@@ -43,7 +48,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
import com.leinardi.android.speeddial.SpeedDialView
import java8.nio.file.Path
import java8.nio.file.Paths
@@ -52,6 +56,7 @@ import me.zhanghai.android.files.R
import me.zhanghai.android.files.app.application
import me.zhanghai.android.files.app.clipboardManager
import me.zhanghai.android.files.compat.checkSelfPermissionCompat
+import me.zhanghai.android.files.compat.setGroupDividerEnabledCompat
import me.zhanghai.android.files.databinding.FileListFragmentAppBarIncludeBinding
import me.zhanghai.android.files.databinding.FileListFragmentBinding
import me.zhanghai.android.files.databinding.FileListFragmentBottomBarIncludeBinding
@@ -61,6 +66,7 @@ import me.zhanghai.android.files.databinding.FileListFragmentSpeedDialIncludeBin
import me.zhanghai.android.files.file.FileItem
import me.zhanghai.android.files.file.MimeType
import me.zhanghai.android.files.file.asMimeTypeOrNull
+import me.zhanghai.android.files.file.extension
import me.zhanghai.android.files.file.fileProviderUri
import me.zhanghai.android.files.file.isApk
import me.zhanghai.android.files.file.isImage
@@ -79,12 +85,14 @@ import me.zhanghai.android.files.settings.Settings
import me.zhanghai.android.files.terminal.Terminal
import me.zhanghai.android.files.ui.AppBarLayoutExpandHackListener
import me.zhanghai.android.files.ui.CoordinatorAppBarLayout
+import me.zhanghai.android.files.ui.DrawerLayoutOnBackPressedCallback
import me.zhanghai.android.files.ui.FixQueryChangeSearchView
import me.zhanghai.android.files.ui.OverlayToolbarActionMode
import me.zhanghai.android.files.ui.PersistentBarLayout
import me.zhanghai.android.files.ui.PersistentBarLayoutToolbarActionMode
import me.zhanghai.android.files.ui.PersistentDrawerLayout
import me.zhanghai.android.files.ui.ScrollingViewOnApplyWindowInsetsListener
+import me.zhanghai.android.files.ui.SpeedDialViewOnBackPressedCallback
import me.zhanghai.android.files.ui.ThemedFastScroller
import me.zhanghai.android.files.ui.ToolbarActionMode
import me.zhanghai.android.files.util.DebouncedRunnable
@@ -93,35 +101,45 @@ import me.zhanghai.android.files.util.Loading
import me.zhanghai.android.files.util.ParcelableArgs
import me.zhanghai.android.files.util.Stateful
import me.zhanghai.android.files.util.Success
+import me.zhanghai.android.files.util.addOnBackPressedCallback
import me.zhanghai.android.files.util.args
+import me.zhanghai.android.files.util.asFileName
+import me.zhanghai.android.files.util.asFileNameOrNull
import me.zhanghai.android.files.util.checkSelfPermission
import me.zhanghai.android.files.util.copyText
import me.zhanghai.android.files.util.create
import me.zhanghai.android.files.util.createInstallPackageIntent
import me.zhanghai.android.files.util.createIntent
+import me.zhanghai.android.files.util.createManageAppAllFilesAccessPermissionIntent
import me.zhanghai.android.files.util.createSendStreamIntent
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
import me.zhanghai.android.files.util.putArgs
+import me.zhanghai.android.files.util.setOnEditorConfirmActionListener
import me.zhanghai.android.files.util.showToast
import me.zhanghai.android.files.util.startActivitySafe
+import me.zhanghai.android.files.util.supportsExternalStorageManager
import me.zhanghai.android.files.util.takeIfNotEmpty
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,
- CreateArchiveDialogFragment.Listener, RenameFileDialogFragment.Listener,
- CreateFileDialogFragment.Listener, CreateDirectoryDialogFragment.Listener,
- NavigateToPathDialogFragment.Listener, NavigationFragment.Listener,
- ShowRequestAllFilesAccessRationaleDialogFragment.Listener,
+ ConfirmReplaceFileDialogFragment.Listener, OpenApkDialogFragment.Listener,
+ ConfirmDeleteFilesDialogFragment.Listener, CreateArchiveDialogFragment.Listener,
+ RenameFileDialogFragment.Listener, CreateFileDialogFragment.Listener,
+ CreateDirectoryDialogFragment.Listener, NavigateToPathDialogFragment.Listener,
+ NavigationFragment.Listener, ShowRequestAllFilesAccessRationaleDialogFragment.Listener,
+ ShowRequestNotificationPermissionRationaleDialogFragment.Listener,
+ ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.Listener,
ShowRequestStoragePermissionRationaleDialogFragment.Listener,
ShowRequestStoragePermissionInSettingsRationaleDialogFragment.Listener {
private val requestAllFilesAccessLauncher = registerForActivityResult(
@@ -131,9 +149,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 }
@@ -150,6 +177,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
private lateinit var bottomActionMode: ToolbarActionMode
+ private lateinit var layoutManager: GridLayoutManager
+
private lateinit var adapter: FileListAdapter
private val debouncedSearchRunnable = DebouncedRunnable(Handler(Looper.getMainLooper()), 1000) {
@@ -197,15 +226,13 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
binding.persistentBarLayout, binding.bottomBarLayout, binding.bottomToolbar
)
val contentLayoutInitialPaddingBottom = binding.contentLayout.paddingBottom
- binding.appBarLayout.addOnOffsetChangedListener(
- OnOffsetChangedListener { _, verticalOffset ->
- binding.contentLayout.updatePaddingRelative(
- bottom = contentLayoutInitialPaddingBottom +
- binding.appBarLayout.totalScrollRange + verticalOffset
- )
- }
- )
- binding.appBarLayout.syncBackgroundElevationTo(binding.overlayToolbar)
+ binding.appBarLayout.addOnOffsetChangedListener { _, verticalOffset ->
+ binding.contentLayout.updatePaddingRelative(
+ bottom = contentLayoutInitialPaddingBottom +
+ binding.appBarLayout.totalScrollRange + verticalOffset
+ )
+ }
+ binding.appBarLayout.syncBackgroundColorTo(binding.overlayToolbar)
binding.breadcrumbLayout.setListener(this)
if (!(activity.hasSw600Dp && activity.isOrientationLandscape)) {
binding.swipeRefreshLayout.setProgressViewEndTarget(
@@ -213,7 +240,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
)
}
binding.swipeRefreshLayout.setOnRefreshListener { this.refresh() }
- binding.recyclerView.layoutManager = GridLayoutManager(activity, /* TODO */ 1)
+ layoutManager = GridLayoutManager(activity, 1)
+ binding.recyclerView.layoutManager = layoutManager
adapter = FileListAdapter(this)
binding.recyclerView.adapter = adapter
val fastScroller = ThemedFastScroller.create(binding.recyclerView)
@@ -227,46 +255,75 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
R.id.action_create_directory -> showCreateDirectoryDialog()
}
// Returning false causes the speed dial to close without animation.
- //return false;
+ //return false
binding.speedDialView.close()
true
}
+ val viewLifecycleOwner = viewLifecycleOwner
+ addOnBackPressedCallback(
+ object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ viewModel.navigateUp()
+ }
+ }
+ .also { callback ->
+ viewModel.breadcrumbLiveData.observe(viewLifecycleOwner) {
+ callback.isEnabled = viewModel.canNavigateUpBreadcrumb
+ }
+ }
+ )
+ addOnBackPressedCallback(overlayActionMode.onBackPressedCallback)
+ addOnBackPressedCallback(SpeedDialViewOnBackPressedCallback(binding.speedDialView))
+ binding.drawerLayout?.let {
+ addOnBackPressedCallback(DrawerLayoutOnBackPressedCallback(it))
+ }
+
if (!viewModel.hasTrail) {
var path = argsPath
val intent = args.intent
var pickOptions: PickOptions? = null
- when (val action = intent.action ?: Intent.ACTION_VIEW) {
+ when (val action = intent.action) {
Intent.ACTION_GET_CONTENT, Intent.ACTION_OPEN_DOCUMENT,
Intent.ACTION_CREATE_DOCUMENT -> {
- val readOnly = action == Intent.ACTION_GET_CONTENT
+ val mode = if (action == Intent.ACTION_CREATE_DOCUMENT) {
+ PickOptions.Mode.CREATE_FILE
+ } else {
+ PickOptions.Mode.OPEN_FILE
+ }
val mimeType = intent.type?.asMimeTypeOrNull() ?: MimeType.ANY
- val extraMimeTypes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES)
- ?.mapNotNull { it.asMimeTypeOrNull() }?.takeIfNotEmpty()
+ val fileName = if (mode == PickOptions.Mode.CREATE_FILE) {
+ intent.getStringExtra(Intent.EXTRA_TITLE)?.asFileNameOrNull()?.value
+ ?: mimeType.extension?.let { "file.$it" } ?: "file"
+ } else {
+ null
+ }
+ val readOnly = action == Intent.ACTION_GET_CONTENT
+ val extraMimeTypes = if (mode == PickOptions.Mode.OPEN_FILE) {
+ intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES)
+ ?.mapNotNull { it.asMimeTypeOrNull() }?.takeIfNotEmpty()
+ } else {
+ null
+ }
val mimeTypes = extraMimeTypes ?: listOf(mimeType)
val localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)
- val allowMultiple = intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
- // TODO: Actually support ACTION_CREATE_DOCUMENT.
- pickOptions = PickOptions(readOnly, false, mimeTypes, localOnly, allowMultiple)
+ val allowMultiple = mode != PickOptions.Mode.CREATE_FILE &&
+ intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
+ pickOptions =
+ PickOptions(mode, fileName, readOnly, mimeTypes, localOnly, allowMultiple)
}
Intent.ACTION_OPEN_DOCUMENT_TREE -> {
val localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false)
- pickOptions = PickOptions(false, true, emptyList(), localOnly, false)
+ pickOptions = PickOptions(
+ PickOptions.Mode.OPEN_DIRECTORY, null, false, emptyList(), localOnly, false
+ )
}
ACTION_VIEW_DOWNLOADS ->
path = Paths.get(
- @Suppress("DEPRECATION")
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
).path
)
- Intent.ACTION_VIEW ->
- if (path != null) {
- val mimeType = intent.type?.asMimeTypeOrNull()
- if (mimeType != null && path.isArchiveFile(mimeType)) {
- path = path.createArchiveRootPath()
- }
- }
else ->
if (path != null) {
val mimeType = intent.type?.asMimeTypeOrNull()
@@ -283,12 +340,6 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
viewModel.pickOptions = pickOptions
}
}
- 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)
@@ -296,9 +347,18 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
viewModel.breadcrumbLiveData.observe(viewLifecycleOwner) {
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.sortPathSpecificLiveData.observe(viewLifecycleOwner) {
- onSortPathSpecificChanged(it)
+ viewModel.viewSortPathSpecificLiveData.observe(viewLifecycleOwner) {
+ onViewSortPathSpecificChanged(it)
}
viewModel.pickOptionsLiveData.observe(viewLifecycleOwner) { onPickOptionsChanged(it) }
viewModel.selectedFilesLiveData.observe(viewLifecycleOwner) { onSelectedFilesChanged(it) }
@@ -313,13 +373,19 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
override fun onResume() {
super.onResume()
- ensureStorageAccess()
+ if (!viewModel.isNotificationPermissionRequested) {
+ ensureStorageAccess()
+ }
+ if (!viewModel.isStorageAccessRequested) {
+ ensureNotificationPermission()
+ }
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
menuBinding = MenuBinding.inflate(menu, inflater)
+ menuBinding.viewSortItem.subMenu!!.setGroupDividerEnabledCompat(true)
setUpSearchView()
}
@@ -372,7 +438,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
- updateSortMenuItems()
+ updateViewSortMenuItems()
updateSelectAllMenuItem()
updateShowHiddenFilesMenuItem()
}
@@ -388,6 +454,14 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
true
}
+ R.id.action_view_list -> {
+ viewModel.viewType = FileViewType.LIST
+ true
+ }
+ R.id.action_view_grid -> {
+ viewModel.viewType = FileViewType.GRID
+ true
+ }
R.id.action_sort_by_name -> {
viewModel.setSortBy(By.NAME)
true
@@ -418,8 +492,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
viewModel.setSortDirectoriesFirst(!menuBinding.sortDirectoriesFirstItem.isChecked)
true
}
- R.id.action_sort_path_specific -> {
- viewModel.isSortPathSpecific = !menuBinding.sortPathSpecificItem.isChecked
+ R.id.action_view_sort_path_specific -> {
+ viewModel.isViewSortPathSpecific = !menuBinding.viewSortPathSpecificItem.isChecked
true
}
R.id.action_new_task -> {
@@ -470,26 +544,24 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
}
- fun onBackPressed(): Boolean {
- val drawerLayout = binding.drawerLayout
- if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.START)) {
- drawerLayout.closeDrawer(GravityCompat.START)
- return true
- }
- if (binding.speedDialView.isOpen) {
- binding.speedDialView.close()
- return true
+ fun onKeyShortcut(keyCode: Int, event: KeyEvent): Boolean {
+ if (bottomActionMode.isActive) {
+ val menu = bottomActionMode.menu
+ menu.setQwertyMode(
+ KeyCharacterMap.load(event.deviceId).keyboardType != KeyCharacterMap.NUMERIC
+ )
+ if (menu.performShortcut(keyCode, event, 0)) {
+ return true
+ }
}
if (overlayActionMode.isActive) {
- overlayActionMode.finish()
- return true
- }
- 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
+ val menu = overlayActionMode.menu
+ menu.setQwertyMode(
+ KeyCharacterMap.load(event.deviceId).keyboardType != KeyCharacterMap.NUMERIC
+ )
+ if (menu.performShortcut(keyCode, event, 0)) {
+ return true
+ }
}
return false
}
@@ -502,6 +574,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
it.closeDrawer(GravityCompat.START)
}
}
+ updateSpanCount()
}
private fun onCurrentPathChanged(path: Path) {
@@ -510,7 +583,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
private fun onSearchViewExpandedChanged(expanded: Boolean) {
- updateSortMenuItems()
+ updateViewSortMenuItems()
}
private fun onFileListChanged(stateful: Stateful>) {
@@ -543,8 +616,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
adapter.clear()
}
if (stateful is Success) {
- viewModel.pendingState
- ?.let { binding.recyclerView.layoutManager!!.onRestoreInstanceState(it) }
+ viewModel.pendingState?.let { layoutManager.onRestoreInstanceState(it) }
}
}
@@ -575,24 +647,51 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
}
+ private fun onViewTypeChanged(viewType: FileViewType) {
+ updateSpanCount()
+ adapter.viewType = viewType
+ updateViewSortMenuItems()
+ }
+
+ private fun updateSpanCount() {
+ layoutManager.spanCount = when (viewModel.viewType) {
+ FileViewType.LIST -> 1
+ FileViewType.GRID -> {
+ var widthDp = resources.configuration.screenWidthDp
+ val persistentDrawerLayout = binding.persistentDrawerLayout
+ if (persistentDrawerLayout != null &&
+ persistentDrawerLayout.isDrawerOpen(GravityCompat.START)) {
+ widthDp -= getDimensionDp(R.dimen.navigation_max_width).roundToInt()
+ }
+ (widthDp / 180).coerceAtLeast(2)
+ }
+ }
+ }
+
private fun onSortOptionsChanged(sortOptions: FileSortOptions) {
- adapter.comparator = sortOptions.createComparator()
- updateSortMenuItems()
+ adapter.sortOptions = sortOptions
+ updateViewSortMenuItems()
}
- private fun onSortPathSpecificChanged(pathSpecific: Boolean) {
- updateSortMenuItems()
+ private fun onViewSortPathSpecificChanged(pathSpecific: Boolean) {
+ updateViewSortMenuItems()
}
- private fun updateSortMenuItems() {
+ private fun updateViewSortMenuItems() {
if (!this::menuBinding.isInitialized) {
return
}
val searchViewExpanded = viewModel.isSearchViewExpanded
- menuBinding.sortItem.isVisible = !searchViewExpanded
+ menuBinding.viewSortItem.isVisible = !searchViewExpanded
if (searchViewExpanded) {
return
}
+ val viewType = viewModel.viewType
+ val checkedViewTypeItem = when (viewType) {
+ FileViewType.LIST -> menuBinding.viewListItem
+ FileViewType.GRID -> menuBinding.viewGridItem
+ }
+ checkedViewTypeItem.isChecked = true
val sortOptions = viewModel.sortOptions
val checkedSortByItem = when (sortOptions.by) {
By.NAME -> menuBinding.sortByNameItem
@@ -603,12 +702,12 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
checkedSortByItem.isChecked = true
menuBinding.sortOrderAscendingItem.isChecked = sortOptions.order == Order.ASCENDING
menuBinding.sortDirectoriesFirstItem.isChecked = sortOptions.isDirectoriesFirst
- menuBinding.sortPathSpecificItem.isChecked = viewModel.isSortPathSpecific
+ menuBinding.viewSortPathSpecificItem.isChecked = viewModel.isViewSortPathSpecific
}
private fun navigateUp() {
collapseSearchView()
- viewModel.navigateUp(true)
+ viewModel.navigateUp()
}
private fun showNavigateToPathDialog() {
@@ -667,7 +766,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
override fun navigateTo(path: Path) {
collapseSearchView()
- val state = binding.recyclerView.layoutManager!!.onSaveInstanceState()
+ val state = layoutManager.onSaveInstanceState()
viewModel.navigateTo(state!!, path)
}
@@ -685,13 +784,14 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
val title = if (pickOptions == null) {
getString(R.string.file_list_title)
} else {
- val titleRes = if (pickOptions.pickDirectory) {
- R.plurals.file_list_title_pick_directory
- } else {
- R.plurals.file_list_title_pick_file
- }
val count = if (pickOptions.allowMultiple) Int.MAX_VALUE else 1
- getQuantityString(titleRes, count)
+ when (pickOptions.mode) {
+ PickOptions.Mode.OPEN_FILE ->
+ getQuantityString(R.plurals.file_list_title_open_file, count)
+ PickOptions.Mode.CREATE_FILE -> getString(R.string.file_list_title_create_file)
+ PickOptions.Mode.OPEN_DIRECTORY ->
+ getQuantityString(R.plurals.file_list_title_open_directory, count)
+ }
}
requireActivity().title = title
updateSelectAllMenuItem()
@@ -730,7 +830,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
flags = flags or (Intent.FLAG_GRANT_WRITE_URI_PERMISSION
or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
}
- if (pickOptions.pickDirectory) {
+ if (pickOptions.mode == PickOptions.Mode.OPEN_DIRECTORY) {
flags = flags or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
}
addFlags(flags)
@@ -759,6 +859,12 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
overlayActionMode.title = getString(R.string.file_list_select_title_format, files.size)
overlayActionMode.setMenuResource(R.menu.file_list_pick)
val menu = overlayActionMode.menu
+ val isOpen = when (pickOptions.mode) {
+ PickOptions.Mode.OPEN_FILE, PickOptions.Mode.OPEN_DIRECTORY -> true
+ PickOptions.Mode.CREATE_FILE -> false
+ }
+ menu.findItem(R.id.action_open).isVisible = isOpen
+ menu.findItem(R.id.action_create).isVisible = !isOpen
menu.findItem(R.id.action_select_all).isVisible = pickOptions.allowMultiple
} else {
overlayActionMode.title = getString(R.string.file_list_select_title_format, files.size)
@@ -776,9 +882,15 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
)
.setTitle(
- if (areAllFilesArchivePaths) R.string.file_list_select_action_extract else R.string.copy
+ if (areAllFilesArchivePaths) {
+ R.string.file_list_select_action_extract
+ } else {
+ R.string.copy
+ }
)
menu.findItem(R.id.action_delete).isVisible = !isAnyFileReadOnly
+ val areAllFilesArchiveFiles = files.all { it.isArchiveFile }
+ menu.findItem(R.id.action_extract).isVisible = areAllFilesArchiveFiles
val isCurrentPathReadOnly = viewModel.currentPath.fileSystem.isReadOnly
menu.findItem(R.id.action_archive).isVisible = !isCurrentPathReadOnly
}
@@ -788,29 +900,28 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
AppBarLayoutExpandHackListener(binding.recyclerView)
)
overlayActionMode.start(object : ToolbarActionMode.Callback {
- override fun onToolbarActionModeStarted(toolbarActionMode: ToolbarActionMode) {}
-
- override fun onToolbarActionModeItemClicked(
+ override fun onToolbarActionModeMenuItemClicked(
toolbarActionMode: ToolbarActionMode,
item: MenuItem
- ): Boolean = onOverlayActionModeItemClicked(toolbarActionMode, item)
+ ): Boolean = onOverlayActionModeMenuItemClicked(item)
override fun onToolbarActionModeFinished(toolbarActionMode: ToolbarActionMode) {
- onOverlayActionModeFinished(toolbarActionMode)
+ onOverlayActionModeFinished()
}
})
}
}
- private fun onOverlayActionModeItemClicked(
- toolbarActionMode: ToolbarActionMode,
- item: MenuItem
- ): Boolean =
+ private fun onOverlayActionModeMenuItemClicked(item: MenuItem): Boolean =
when (item.itemId) {
- R.id.action_pick -> {
+ R.id.action_open -> {
pickFiles(viewModel.selectedFiles)
true
}
+ R.id.action_create -> {
+ confirmReplaceFile(viewModel.selectedFiles.single())
+ true
+ }
R.id.action_cut -> {
cutFiles(viewModel.selectedFiles)
true
@@ -823,6 +934,10 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
confirmDeleteFiles(viewModel.selectedFiles)
true
}
+ R.id.action_extract -> {
+ extractFiles(viewModel.selectedFiles)
+ true
+ }
R.id.action_archive -> {
showCreateArchiveDialog(viewModel.selectedFiles)
true
@@ -838,10 +953,25 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
else -> false
}
- private fun onOverlayActionModeFinished(toolbarActionMode: ToolbarActionMode) {
+ private fun onOverlayActionModeFinished() {
viewModel.clearSelectedFiles()
}
+ private fun confirmReplaceFile(file: FileItem, setFileName: Boolean = true) {
+ if (setFileName) {
+ val fileName = file.name
+ binding.bottomCreateFileNameEdit.setText(fileName)
+ binding.bottomCreateFileNameEdit.setSelection(
+ 0, fileName.asFileName().baseName.length
+ )
+ }
+ ConfirmReplaceFileDialogFragment.show(file, this)
+ }
+
+ override fun replaceFile(file: FileItem) {
+ pickFiles(fileItemSetOf(file))
+ }
+
private fun cutFiles(files: FileItemSet) {
viewModel.addToPasteState(false, files)
viewModel.selectFiles(files, false)
@@ -861,6 +991,11 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
viewModel.selectFiles(files, false)
}
+ private fun extractFiles(files: FileItemSet) {
+ copyFiles(files.mapTo(fileItemSetOf()) { it.createDummyArchiveRoot() })
+ viewModel.selectFiles(files, false)
+ }
+
private fun showCreateArchiveDialog(files: FileItemSet) {
CreateArchiveDialogFragment.show(files, this)
}
@@ -868,12 +1003,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)
}
@@ -894,18 +1030,45 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
private fun updateBottomToolbar() {
val pickOptions = viewModel.pickOptions
if (pickOptions != null) {
- if (!pickOptions.pickDirectory) {
- if (bottomActionMode.isActive) {
- bottomActionMode.finish()
+ bottomActionMode.setMenuResource(R.menu.file_list_pick_bottom)
+ val menu = bottomActionMode.menu
+ when (pickOptions.mode) {
+ PickOptions.Mode.CREATE_FILE -> {
+ bottomActionMode.title = null
+ binding.bottomCreateFileNameEdit.isVisible = true
+ val createMenuItem = menu.findItem(R.id.action_create)
+ binding.bottomCreateFileNameEdit.setOnEditorConfirmActionListener {
+ onBottomActionModeMenuItemClicked(createMenuItem)
+ }
+ if (!viewModel.isCreateFileNameEditInitialized) {
+ val fileName = pickOptions.fileName!!
+ binding.bottomCreateFileNameEdit.setText(fileName)
+ binding.bottomCreateFileNameEdit.setSelection(
+ 0, fileName.asFileName().baseName.length
+ )
+ binding.bottomCreateFileNameEdit.requestFocus()
+ viewModel.isCreateFileNameEditInitialized = true
+ }
+ menu.findItem(R.id.action_open).isVisible = false
+ createMenuItem.isVisible = true
+ }
+ PickOptions.Mode.OPEN_DIRECTORY -> {
+ val path = viewModel.currentPath
+ val navigationRoot = NavigationRootMapLiveData.valueCompat[path]
+ val name = navigationRoot?.getName(requireContext()) ?: path.name
+ bottomActionMode.title =
+ getString(R.string.file_list_open_current_directory_format, name)
+ binding.bottomCreateFileNameEdit.isVisible = false
+ menu.findItem(R.id.action_open).isVisible = true
+ menu.findItem(R.id.action_create).isVisible = false
+ }
+ else -> {
+ if (bottomActionMode.isActive) {
+ bottomActionMode.finish()
+ }
+ return
}
- return
}
- bottomActionMode.setNavigationIcon(R.drawable.check_icon_control_normal_24dp)
- val path = viewModel.currentPath
- val navigationRoot = NavigationRootMapLiveData.valueCompat[path]
- val name = navigationRoot?.getName(requireContext()) ?: path.name
- bottomActionMode.title =
- getString(R.string.file_list_select_current_directory_format, name)
} else {
val pasteState = viewModel.pasteState
val files = pasteState.files
@@ -915,7 +1078,6 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
return
}
- bottomActionMode.setNavigationIcon(R.drawable.close_icon_control_normal_24dp)
val areAllFilesArchivePaths = files.all { it.path.isArchivePath }
bottomActionMode.title = getString(
if (pasteState.copy) {
@@ -928,6 +1090,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
R.string.file_list_paste_move_title_format
}, files.size
)
+ binding.bottomCreateFileNameEdit.isVisible = false
bottomActionMode.setMenuResource(R.menu.file_list_paste)
val isCurrentPathReadOnly = viewModel.currentPath.fileSystem.isReadOnly
bottomActionMode.menu.findItem(R.id.action_paste)
@@ -938,25 +1101,54 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
if (!bottomActionMode.isActive) {
bottomActionMode.start(object : ToolbarActionMode.Callback {
- override fun onToolbarActionModeStarted(toolbarActionMode: ToolbarActionMode) {}
+ override fun onToolbarNavigationIconClicked(toolbarActionMode: ToolbarActionMode) {
+ onBottomToolbarNavigationIconClicked()
+ }
- override fun onToolbarActionModeItemClicked(
+ override fun onToolbarActionModeMenuItemClicked(
toolbarActionMode: ToolbarActionMode,
item: MenuItem
- ): Boolean = onBottomActionModeItemClicked(toolbarActionMode, item)
+ ): Boolean = onBottomActionModeMenuItemClicked(item)
override fun onToolbarActionModeFinished(toolbarActionMode: ToolbarActionMode) {
- onBottomActionModeFinished(toolbarActionMode)
+ onBottomActionModeFinished()
}
})
}
}
- private fun onBottomActionModeItemClicked(
- toolbarActionMode: ToolbarActionMode,
- item: MenuItem
- ): Boolean =
+ private fun onBottomToolbarNavigationIconClicked() {
+ val pickOptions = viewModel.pickOptions
+ if (pickOptions != null) {
+ requireActivity().finish()
+ } else {
+ bottomActionMode.finish()
+ }
+ }
+
+ private fun onBottomActionModeMenuItemClicked(item: MenuItem): Boolean =
when (item.itemId) {
+ R.id.action_open -> {
+ pickPaths(linkedSetOf(viewModel.currentPath))
+ true
+ }
+ R.id.action_create -> {
+ val fileName = binding.bottomCreateFileNameEdit.text.toString()
+ if (fileName.isEmpty()) {
+ showToast(R.string.file_list_create_file_name_error_empty)
+ } else if (fileName.asFileNameOrNull() == null) {
+ showToast(R.string.file_list_create_file_name_error_invalid)
+ } else {
+ val file = getFileWithName(fileName)
+ if (file != null) {
+ confirmReplaceFile(file, false)
+ } else {
+ val path = viewModel.currentPath.resolve(fileName)
+ pickPaths(linkedSetOf(path))
+ }
+ }
+ true
+ }
R.id.action_paste -> {
pasteFiles(currentPath)
true
@@ -964,13 +1156,9 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
else -> false
}
- private fun onBottomActionModeFinished(toolbarActionMode: ToolbarActionMode) {
+ private fun onBottomActionModeFinished() {
val pickOptions = viewModel.pickOptions
- if (pickOptions != null) {
- if (pickOptions.pickDirectory) {
- pickPaths(linkedSetOf(viewModel.currentPath))
- }
- } else {
+ if (pickOptions == null) {
viewModel.clearPasteState()
}
}
@@ -990,7 +1178,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
@@ -1013,8 +1201,12 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
if (pickOptions != null) {
if (file.attributes.isDirectory) {
navigateTo(file.path)
- } else if (!pickOptions.pickDirectory) {
- pickFiles(fileItemSetOf(file))
+ } else {
+ when (pickOptions.mode) {
+ PickOptions.Mode.OPEN_FILE -> pickFiles(fileItemSetOf(file))
+ PickOptions.Mode.CREATE_FILE -> confirmReplaceFile(file)
+ PickOptions.Mode.OPEN_DIRECTORY -> {}
+ }
}
return
}
@@ -1098,7 +1290,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
var paths = mutableListOf()
// 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..= Build.VERSION_CODES.R) {
+ if (Environment::class.supportsExternalStorageManager()) {
if (!Environment.isExternalStorageManager()) {
ShowRequestAllFilesAccessRationaleDialogFragment.show(this)
viewModel.isStorageAccessRequested = true
}
} 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
)) {
@@ -1288,18 +1486,37 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
}
}
- 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)
}
@@ -1307,24 +1524,103 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
if (isGranted) {
viewModel.isStorageAccessRequested = false
refresh()
- } else if (!shouldShowRequestPermissionRationale(
- android.Manifest.permission.WRITE_EXTERNAL_STORAGE
- )) {
+ } else if (shouldShowRequestPermissionRationale(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+ )) {
+ ShowRequestStoragePermissionRationaleDialogFragment.show(this)
+ } else {
ShowRequestStoragePermissionInSettingsRationaleDialogFragment.show(this)
}
}
- 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() {
+ if (viewModel.isNotificationPermissionRequested) {
+ return
+ }
+ 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
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ 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)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private fun onRequestNotificationPermissionResult(isGranted: Boolean) {
+ if (isGranted) {
+ viewModel.isNotificationPermissionRequested = false
+ } else if (shouldShowRequestPermissionRationale(
+ android.Manifest.permission.POST_NOTIFICATIONS
+ )) {
+ ShowRequestNotificationPermissionRationaleDialogFragment.show(this)
+ } else {
+ ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.show(this)
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ override fun onShowRequestNotificationPermissionInSettingsRationaleResult(
+ shouldRequest: Boolean
+ ) {
+ if (shouldRequest) {
+ requestNotificationPermissionInSettings()
+ } else {
+ viewModel.isNotificationPermissionRequested = false
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private fun requestNotificationPermissionInSettings() {
+ requestNotificationPermissionInSettingsLauncher.launch(Unit)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ private fun onRequestNotificationPermissionInSettingsResult(isGranted: Boolean) {
+ if (isGranted) {
+ viewModel.isNotificationPermissionRequested = false
+ }
+ }
+
companion object {
private const val ACTION_VIEW_DOWNLOADS =
"me.zhanghai.android.files.intent.action.VIEW_DOWNLOADS"
@@ -1335,17 +1631,14 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
private class RequestAllFilesAccessContract : ActivityResultContract() {
@RequiresApi(Build.VERSION_CODES.R)
override fun createIntent(context: Context, input: Unit): Intent =
- Intent(
- android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
- Uri.fromParts("package", context.packageName, null)
- )
+ Environment::class.createManageAppAllFilesAccessPermissionIntent(context.packageName)
@RequiresApi(Build.VERSION_CODES.R)
override fun parseResult(resultCode: Int, intent: Intent?): Boolean =
Environment.isExternalStorageManager()
}
- private class RequestStoragePermissionInSettingsContract
+ private class RequestPermissionInSettingsContract(private val permissionName: String)
: ActivityResultContract() {
override fun createIntent(context: Context, input: Unit): Intent =
Intent(
@@ -1354,9 +1647,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
@@ -1379,6 +1671,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
val recyclerView: RecyclerView,
val bottomBarLayout: ViewGroup,
val bottomToolbar: Toolbar,
+ val bottomCreateFileNameEdit: EditText,
val speedDialView: SpeedDialView
) {
companion object {
@@ -1402,7 +1695,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
contentBinding.progress, contentBinding.errorText, contentBinding.emptyView,
contentBinding.swipeRefreshLayout, contentBinding.recyclerView,
bottomBarBinding.bottomBarLayout, bottomBarBinding.bottomToolbar,
- speedDialBinding.speedDialView
+ bottomBarBinding.bottomCreateFileNameEdit, speedDialBinding.speedDialView
)
}
}
@@ -1411,14 +1704,16 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
private class MenuBinding private constructor(
val menu: Menu,
val searchItem: MenuItem,
- val sortItem: MenuItem,
+ val viewSortItem: MenuItem,
+ val viewListItem: MenuItem,
+ val viewGridItem: MenuItem,
val sortByNameItem: MenuItem,
val sortByTypeItem: MenuItem,
val sortBySizeItem: MenuItem,
val sortByLastModifiedItem: MenuItem,
val sortOrderAscendingItem: MenuItem,
val sortDirectoriesFirstItem: MenuItem,
- val sortPathSpecificItem: MenuItem,
+ val viewSortPathSpecificItem: MenuItem,
val selectAllItem: MenuItem,
val showHiddenFilesItem: MenuItem
) {
@@ -1426,14 +1721,15 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
fun inflate(menu: Menu, inflater: MenuInflater): MenuBinding {
inflater.inflate(R.menu.file_list, menu)
return MenuBinding(
- menu, menu.findItem(R.id.action_search), menu.findItem(R.id.action_sort),
+ menu, menu.findItem(R.id.action_search), menu.findItem(R.id.action_view_sort),
+ menu.findItem(R.id.action_view_list), menu.findItem(R.id.action_view_grid),
menu.findItem(R.id.action_sort_by_name),
menu.findItem(R.id.action_sort_by_type),
menu.findItem(R.id.action_sort_by_size),
menu.findItem(R.id.action_sort_by_last_modified),
menu.findItem(R.id.action_sort_order_ascending),
menu.findItem(R.id.action_sort_directories_first),
- menu.findItem(R.id.action_sort_path_specific),
+ menu.findItem(R.id.action_view_sort_path_specific),
menu.findItem(R.id.action_select_all),
menu.findItem(R.id.action_show_hidden_files)
)
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 cc0c8be9b..bafa4c659 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
@@ -34,12 +34,7 @@ class FileListViewModel : ViewModel() {
fun resetTo(path: Path) = trailLiveData.resetTo(path)
- fun navigateUp(overrideBreadcrumb: Boolean): Boolean =
- if (!overrideBreadcrumb && breadcrumbLiveData.valueCompat.selectedIndex == 0) {
- false
- } else {
- trailLiveData.navigateUp()
- }
+ fun navigateUp(): Boolean = trailLiveData.navigateUp()
val currentPathLiveData = trailLiveData.map { it.currentPath }
val currentPath: Path
@@ -102,6 +97,16 @@ class FileListViewModel : ViewModel() {
}
val breadcrumbLiveData: LiveData = BreadcrumbLiveData(trailLiveData)
+ val canNavigateUpBreadcrumb: Boolean
+ get() = breadcrumbLiveData.valueCompat.selectedIndex > 0
+
+ private val _viewTypeLiveData = FileViewTypeLiveData(currentPathLiveData)
+ val viewTypeLiveData: LiveData = _viewTypeLiveData
+ var viewType: FileViewType
+ get() = _viewTypeLiveData.valueCompat
+ set(value) {
+ _viewTypeLiveData.putValue(value)
+ }
private val _sortOptionsLiveData = FileSortOptionsLiveData(currentPathLiveData)
val sortOptionsLiveData: LiveData = _sortOptionsLiveData
@@ -115,13 +120,14 @@ class FileListViewModel : ViewModel() {
fun setSortDirectoriesFirst(isDirectoriesFirst: Boolean) =
_sortOptionsLiveData.putIsDirectoriesFirst(isDirectoriesFirst)
- private val _sortPathSpecificLiveData = FileSortPathSpecificLiveData(currentPathLiveData)
- val sortPathSpecificLiveData: LiveData
- get() = _sortPathSpecificLiveData
- var isSortPathSpecific: Boolean
- get() = _sortPathSpecificLiveData.valueCompat
+ private val _viewSortPathSpecificLiveData =
+ FileViewSortPathSpecificLiveData(currentPathLiveData)
+ val viewSortPathSpecificLiveData: LiveData
+ get() = _viewSortPathSpecificLiveData
+ var isViewSortPathSpecific: Boolean
+ get() = _viewSortPathSpecificLiveData.valueCompat
set(value) {
- _sortPathSpecificLiveData.putValue(value)
+ _viewSortPathSpecificLiveData.putValue(value)
}
private val _pickOptionsLiveData = MutableLiveData()
@@ -133,6 +139,8 @@ class FileListViewModel : ViewModel() {
_pickOptionsLiveData.value = value
}
+ var isCreateFileNameEditInitialized: Boolean = false
+
private val _selectedFilesLiveData = MutableLiveData(fileItemSetOf())
val selectedFilesLiveData: LiveData
get() = _selectedFilesLiveData
@@ -218,6 +226,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/FileSortPathSpecificLiveData.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileViewSortPathSpecificLiveData.kt
similarity index 63%
rename from app/src/main/java/me/zhanghai/android/files/filelist/FileSortPathSpecificLiveData.kt
rename to app/src/main/java/me/zhanghai/android/files/filelist/FileViewSortPathSpecificLiveData.kt
index 2791a1b16..3db90ff38 100644
--- a/app/src/main/java/me/zhanghai/android/files/filelist/FileSortPathSpecificLiveData.kt
+++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileViewSortPathSpecificLiveData.kt
@@ -13,11 +13,12 @@ import me.zhanghai.android.files.settings.SettingLiveData
import me.zhanghai.android.files.settings.Settings
import me.zhanghai.android.files.util.valueCompat
-class FileSortPathSpecificLiveData(pathLiveData: LiveData) : MediatorLiveData() {
+class FileViewSortPathSpecificLiveData(pathLiveData: LiveData) : MediatorLiveData() {
+ private lateinit var pathViewTypeLiveData: SettingLiveData
private lateinit var pathSortOptionsLiveData: SettingLiveData
private fun loadValue() {
- val value = pathSortOptionsLiveData.value != null
+ val value = pathViewTypeLiveData.value != null || pathSortOptionsLiveData.value != null
if (this.value != value) {
this.value = value
}
@@ -25,10 +26,16 @@ class FileSortPathSpecificLiveData(pathLiveData: LiveData) : MediatorLiveD
fun putValue(value: Boolean) {
if (value) {
+ if (pathViewTypeLiveData.value == null) {
+ pathViewTypeLiveData.putValue(Settings.FILE_LIST_VIEW_TYPE.valueCompat)
+ }
if (pathSortOptionsLiveData.value == null) {
pathSortOptionsLiveData.putValue(Settings.FILE_LIST_SORT_OPTIONS.valueCompat)
}
} else {
+ if (pathViewTypeLiveData.value != null) {
+ pathViewTypeLiveData.putValue(null)
+ }
if (pathSortOptionsLiveData.value != null) {
pathSortOptionsLiveData.putValue(null)
}
@@ -37,10 +44,15 @@ class FileSortPathSpecificLiveData(pathLiveData: LiveData) : MediatorLiveD
init {
addSource(pathLiveData) { path: Path ->
+ if (this::pathViewTypeLiveData.isInitialized) {
+ removeSource(pathViewTypeLiveData)
+ }
if (this::pathSortOptionsLiveData.isInitialized) {
removeSource(pathSortOptionsLiveData)
}
+ pathViewTypeLiveData = PathSettings.getFileListViewType(path)
pathSortOptionsLiveData = PathSettings.getFileListSortOptions(path)
+ addSource(pathViewTypeLiveData) { loadValue() }
addSource(pathSortOptionsLiveData) { loadValue() }
}
}
diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileViewType.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileViewType.kt
new file mode 100644
index 000000000..ba7bad764
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileViewType.kt
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.filelist
+
+enum class FileViewType {
+ LIST,
+ GRID
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileViewTypeLiveData.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileViewTypeLiveData.kt
new file mode 100644
index 000000000..ab0e35705
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileViewTypeLiveData.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.filelist
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import java8.nio.file.Path
+import me.zhanghai.android.files.settings.PathSettings
+import me.zhanghai.android.files.settings.SettingLiveData
+import me.zhanghai.android.files.settings.Settings
+import me.zhanghai.android.files.util.valueCompat
+
+class FileViewTypeLiveData(pathLiveData: LiveData) : MediatorLiveData() {
+ private lateinit var pathViewTypeLiveData: SettingLiveData
+
+ private fun loadValue() {
+ if (!this::pathViewTypeLiveData.isInitialized) {
+ // Not yet initialized.
+ return
+ }
+ val value = pathViewTypeLiveData.value ?: Settings.FILE_LIST_VIEW_TYPE.valueCompat
+ if (this.value != value) {
+ this.value = value
+ }
+ }
+
+ fun putValue(value: FileViewType) {
+ if (pathViewTypeLiveData.value != null) {
+ pathViewTypeLiveData.putValue(value)
+ } else {
+ Settings.FILE_LIST_VIEW_TYPE.putValue(value)
+ }
+ }
+
+ init {
+ addSource(Settings.FILE_LIST_VIEW_TYPE) { loadValue() }
+ addSource(pathLiveData) { path: Path ->
+ if (this::pathViewTypeLiveData.isInitialized) {
+ removeSource(pathViewTypeLiveData)
+ }
+ pathViewTypeLiveData = PathSettings.getFileListViewType(path)
+ addSource(pathViewTypeLiveData) { loadValue() }
+ }
+ }
+}
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
diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/PickOptions.kt b/app/src/main/java/me/zhanghai/android/files/filelist/PickOptions.kt
index ab4f72f9a..1082d6fe1 100644
--- a/app/src/main/java/me/zhanghai/android/files/filelist/PickOptions.kt
+++ b/app/src/main/java/me/zhanghai/android/files/filelist/PickOptions.kt
@@ -8,9 +8,16 @@ package me.zhanghai.android.files.filelist
import me.zhanghai.android.files.file.MimeType
class PickOptions(
+ val mode: Mode,
+ val fileName: String?,
val readOnly: Boolean,
- val pickDirectory: Boolean,
val mimeTypes: List,
val localOnly: Boolean,
val allowMultiple: Boolean
-)
+) {
+ enum class Mode {
+ OPEN_FILE,
+ CREATE_FILE,
+ OPEN_DIRECTORY
+ }
+}
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
new file mode 100644
index 000000000..a240c0bfe
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+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
+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_permanently_denied_message)
+ .setPositiveButton(R.string.open_settings) { _, _ ->
+ listener.onShowRequestNotificationPermissionInSettingsRationaleResult(true)
+ }
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
+ listener.onShowRequestNotificationPermissionInSettingsRationaleResult(false)
+ }
+ .create()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+
+ listener.onShowRequestNotificationPermissionInSettingsRationaleResult(false)
+ }
+
+ companion object {
+ fun show(fragment: Fragment) {
+ ShowRequestNotificationPermissionInSettingsRationaleDialogFragment().show(fragment)
+ }
+ }
+
+ interface Listener {
+ 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
new file mode 100644
index 000000000..1f589ad84
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+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
+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.onShowRequestNotificationPermissionRationaleResult(true)
+ }
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
+ listener.onShowRequestNotificationPermissionRationaleResult(false)
+ }
+ .create()
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ super.onCancel(dialog)
+
+ listener.onShowRequestNotificationPermissionRationaleResult(false)
+ }
+
+ companion object {
+ fun show(fragment: Fragment) {
+ ShowRequestNotificationPermissionRationaleDialogFragment().show(fragment)
+ }
+ }
+
+ interface Listener {
+ 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..68ceb6828 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
@@ -19,14 +20,22 @@ class ShowRequestStoragePermissionInSettingsRationaleDialogFragment : AppCompatD
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext(), theme)
- .setMessage(R.string.storage_permission_rationale_message)
+ .setMessage(R.string.storage_permission_permanently_denied_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)
}
}
diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/TrailData.kt b/app/src/main/java/me/zhanghai/android/files/filelist/TrailData.kt
index 58d3afd74..f0638efd7 100644
--- a/app/src/main/java/me/zhanghai/android/files/filelist/TrailData.kt
+++ b/app/src/main/java/me/zhanghai/android/files/filelist/TrailData.kt
@@ -33,7 +33,7 @@ class TrailData private constructor(
}
}
if (isPrefix) {
- for (index in newTrail.size until trail.size) {
+ for (index in newTrail.size.. Fragment>>()
.apply {
add(R.string.file_properties_basic to { FilePropertiesBasicTabFragment() })
- if (FilePropertiesPermissionsTabFragment.isAvailable(args.file)) {
+ if (FilePropertiesPermissionTabFragment.isAvailable(args.file)) {
add(
- R.string.file_properties_permissions
- to { FilePropertiesPermissionsTabFragment() }
+ R.string.file_properties_permission
+ to { FilePropertiesPermissionTabFragment() }
)
}
if (FilePropertiesImageTabFragment.isAvailable(args.file)) {
@@ -108,6 +110,15 @@ class FilePropertiesDialogFragment : AppCompatDialogFragment() {
}
)
}
+ if (FilePropertiesChecksumTabFragment.isAvailable(args.file)) {
+ add(
+ R.string.file_properties_checksum to {
+ FilePropertiesChecksumTabFragment().putArgs(
+ FilePropertiesChecksumTabFragment.Args(args.file.path)
+ )
+ }
+ )
+ }
}
.map { getString(it.first) to it.second }
.toTypedArray()
@@ -117,6 +128,14 @@ class FilePropertiesDialogFragment : AppCompatDialogFragment() {
binding.tabLayout.setupWithViewPager(binding.viewPager)
}
+ override fun onStart() {
+ super.onStart()
+
+ // AlertDialog (its AlertController) adds FLAG_ALT_FOCUSABLE_IM when the initial custom
+ // view doesn't have any view that returns true for onCheckIsTextEditor().
+ requireDialog().window!!.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
+ }
+
companion object {
fun show(file: FileItem, fragment: Fragment) {
FilePropertiesDialogFragment().putArgs(Args(file)).show(fragment)
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/FilePropertiesTabFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/FilePropertiesTabFragment.kt
index 4fee51170..e577262ef 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/FilePropertiesTabFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/FilePropertiesTabFragment.kt
@@ -12,9 +12,9 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.StringRes
-import androidx.core.view.get
-import androidx.core.view.size
+import androidx.core.view.forEach
import androidx.fragment.app.Fragment
+import androidx.viewbinding.ViewBinding
import me.zhanghai.android.files.databinding.FilePropertiesTabFragmentBinding
import me.zhanghai.android.files.databinding.FilePropertiesTabItemBinding
import me.zhanghai.android.files.util.Failure
@@ -68,29 +68,41 @@ abstract class FilePropertiesTabFragment : Fragment() {
}
}
- protected class ViewBuilder(private val linearLayout: LinearLayout) {
- private var itemCount = 0
+ protected class ViewBuilder(val linearLayout: LinearLayout) {
+ private val scrapViews = mutableMapOf, MutableList>()
+
+ init {
+ linearLayout.forEach { view ->
+ val binding = view.tag as ViewBinding
+ scrapViews.getOrPut(binding.javaClass) { mutableListOf() } += binding
+ }
+ linearLayout.removeAllViews()
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ fun getScrapItemBinding(bindingClass: Class): T? =
+ scrapViews[bindingClass]?.removeLastOrNull() as T?
+
+ fun addView(binding: ViewBinding) {
+ linearLayout.addView(binding.root)
+ }
fun addItemView(
hint: String,
text: String,
onClickListener: ((View) -> Unit)? = null
): TextView {
- val itemBinding = if (itemCount < linearLayout.size) {
- linearLayout[itemCount].tag as FilePropertiesTabItemBinding
- } else {
- FilePropertiesTabItemBinding.inflate(
- linearLayout.context.layoutInflater, linearLayout, true
- ).also { it.root.tag = it }
- }
+ val itemBinding =
+ getScrapItemBinding(FilePropertiesTabItemBinding::class.java)?.also { addView(it) }
+ ?: FilePropertiesTabItemBinding.inflate(
+ linearLayout.context.layoutInflater, linearLayout, true
+ )
+ .also { it.root.tag = it }
itemBinding.textInputLayout.hint = hint
itemBinding.textInputLayout.setDropDown(onClickListener != null)
itemBinding.text.setText(text)
itemBinding.text.setTextIsSelectable(onClickListener == null)
- itemBinding.text.setOnClickListener(
- onClickListener?.let { View.OnClickListener(it) }
- )
- ++itemCount
+ itemBinding.text.setOnClickListener(onClickListener?.let { View.OnClickListener(it) })
return itemBinding.text
}
@@ -101,9 +113,7 @@ abstract class FilePropertiesTabFragment : Fragment() {
): TextView = addItemView(linearLayout.context.getString(hintRes), text, onClickListener)
fun build() {
- for (index in linearLayout.size - 1 downTo itemCount) {
- linearLayout.removeViewAt(index)
- }
+ scrapViews.clear()
}
}
}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/MediaMetadataRetrieverExtensions.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/MediaMetadataRetrieverExtensions.kt
index 6a6ad5a14..c6ff89d3f 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/MediaMetadataRetrieverExtensions.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/MediaMetadataRetrieverExtensions.kt
@@ -6,13 +6,13 @@
package me.zhanghai.android.files.fileproperties
import android.media.MediaMetadataRetriever
-import me.zhanghai.android.files.util.takeIfNotBlank
-import org.threeten.bp.Instant
import java.text.ParsePosition
import java.text.SimpleDateFormat
+import java.time.Instant
import java.util.Locale
import java.util.TimeZone
import kotlin.math.max
+import me.zhanghai.android.files.util.takeIfNotBlank
fun MediaMetadataRetriever.extractMetadataNotBlank(keyCode: Int): String? =
extractMetadata(keyCode)?.takeIfNotBlank()
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/apk/ApkInfoLiveData.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/apk/ApkInfoLiveData.kt
index 21fb87951..85c2c4df0 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/apk/ApkInfoLiveData.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/apk/ApkInfoLiveData.kt
@@ -49,11 +49,11 @@ class ApkInfoLiveData(path: Path) : PathObserverLiveData>(path
val signingCertificates = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// PackageInfo.signatures returns only the oldest certificate if there are
// past certificates on P and above for compatibility.
- packageInfo.signingInfo.apkContentsSigners ?: emptyArray()
+ packageInfo.signingInfo?.apkContentsSigners
} else {
@Suppress("DEPRECATION")
packageInfo.signatures
- }
+ } ?: emptyArray()
val signingCertificateDigests = signingCertificates
.map { it.toByteArray().sha1Digest().toHexString() }
val pastSigningCertificates =
@@ -61,7 +61,7 @@ class ApkInfoLiveData(path: Path) : PathObserverLiveData>(path
val signingInfo = packageInfo.signingInfo
// SigningInfo.getSigningCertificateHistory() may return the current
// certificate if there are no past certificates.
- if (signingInfo.hasPastSigningCertificates()) {
+ if (signingInfo?.hasPastSigningCertificates() == true) {
// SigningInfo.getSigningCertificateHistory() also returns the
// current certificate.
signingInfo.signingCertificateHistory?.toMutableList()
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/apk/FilePropertiesApkTabFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/apk/FilePropertiesApkTabFragment.kt
index 949d67a6c..50f29ce8b 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/apk/FilePropertiesApkTabFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/apk/FilePropertiesApkTabFragment.kt
@@ -6,7 +6,6 @@
package me.zhanghai.android.files.fileproperties.apk
import android.os.Build
-import android.os.Bundle
import java8.nio.file.Path
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
@@ -29,8 +28,8 @@ class FilePropertiesApkTabFragment : FilePropertiesTabFragment() {
private val viewModel by viewModels { { FilePropertiesApkTabViewModel(args.path) } }
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
+ override fun onResume() {
+ super.onResume()
viewModel.apkInfoLiveData.observe(viewLifecycleOwner) { onApkInfoChanged(it) }
}
@@ -50,7 +49,7 @@ class FilePropertiesApkTabFragment : FilePropertiesTabFragment() {
packageInfo.longVersionCodeCompat
)
)
- val applicationInfo = packageInfo.applicationInfo
+ val applicationInfo = packageInfo.applicationInfo!!
// PackageParser didn't return minSdkVersion before N, so it's hard to implement a
// compat version.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -78,7 +77,7 @@ class FilePropertiesApkTabFragment : FilePropertiesTabFragment() {
} else {
{
PermissionListDialogFragment.show(
- packageInfo.requestedPermissions, this@FilePropertiesApkTabFragment
+ packageInfo.requestedPermissions!!, this@FilePropertiesApkTabFragment
)
}
}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/AudioInfo.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/AudioInfo.kt
index 1e93db795..5d6e6e0d2 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/AudioInfo.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/AudioInfo.kt
@@ -5,7 +5,7 @@
package me.zhanghai.android.files.fileproperties.audio
-import org.threeten.bp.Duration
+import java.time.Duration
// @see com.android.providers.media.scan.ModernMediaScanner.scanItemAudio
// @see com.android.documentsui.inspector.MediaView.showAudioData
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/AudioInfoLiveData.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/AudioInfoLiveData.kt
index b277e3912..5e9ae098e 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/AudioInfoLiveData.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/AudioInfoLiveData.kt
@@ -8,6 +8,7 @@ package me.zhanghai.android.files.fileproperties.audio
import android.media.MediaMetadataRetriever
import android.os.AsyncTask
import android.os.Build
+import java.time.Duration
import java8.nio.file.Path
import me.zhanghai.android.files.compat.METADATA_KEY_SAMPLERATE
import me.zhanghai.android.files.compat.use
@@ -19,7 +20,6 @@ import me.zhanghai.android.files.util.Stateful
import me.zhanghai.android.files.util.Success
import me.zhanghai.android.files.util.setDataSource
import me.zhanghai.android.files.util.valueCompat
-import org.threeten.bp.Duration
class AudioInfoLiveData(path: Path) : PathObserverLiveData>(path) {
init {
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/FilePropertiesAudioTabFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/FilePropertiesAudioTabFragment.kt
index 42f3dc34e..99f1c4fc0 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/FilePropertiesAudioTabFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/audio/FilePropertiesAudioTabFragment.kt
@@ -5,7 +5,6 @@
package me.zhanghai.android.files.fileproperties.audio
-import android.os.Bundle
import java8.nio.file.Path
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.WriteWith
@@ -26,8 +25,8 @@ class FilePropertiesAudioTabFragment : FilePropertiesTabFragment() {
private val viewModel by viewModels { { FilePropertiesAudioTabViewModel(args.path) } }
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
+ override fun onResume() {
+ super.onResume()
viewModel.audioInfoLiveData.observe(viewLifecycleOwner) { onAudioInfoChanged(it) }
}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/ChecksumInfo.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/ChecksumInfo.kt
new file mode 100644
index 000000000..4ad96e148
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/ChecksumInfo.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2024 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.fileproperties.checksum
+
+import androidx.annotation.StringRes
+import me.zhanghai.android.files.R
+import java.security.MessageDigest
+
+class ChecksumInfo(val checksums: Map) {
+ enum class Algorithm(@StringRes val nameRes: Int) {
+ CRC32(R.string.file_properties_checksum_crc32),
+ MD5(R.string.file_properties_checksum_md5),
+ SHA1(R.string.file_properties_checksum_sha_1),
+ SHA256(R.string.file_properties_checksum_sha_256),
+ SHA512(R.string.file_properties_checksum_sha_512);
+
+ fun createMessageDigest(): MessageDigest =
+ when (this) {
+ CRC32 -> Crc32MessageDigest()
+ MD5 -> MessageDigest.getInstance("MD5")
+ SHA1 -> MessageDigest.getInstance("SHA-1")
+ SHA256 -> MessageDigest.getInstance("SHA-256")
+ SHA512 -> MessageDigest.getInstance("SHA-512")
+ }
+ }
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/ChecksumInfoLiveData.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/ChecksumInfoLiveData.kt
new file mode 100644
index 000000000..9426db68d
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/ChecksumInfoLiveData.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2024 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.fileproperties.checksum
+
+import android.os.AsyncTask
+import java8.nio.file.Path
+import me.zhanghai.android.files.fileproperties.PathObserverLiveData
+import me.zhanghai.android.files.provider.common.newInputStream
+import me.zhanghai.android.files.util.Failure
+import me.zhanghai.android.files.util.Loading
+import me.zhanghai.android.files.util.Stateful
+import me.zhanghai.android.files.util.Success
+import me.zhanghai.android.files.util.toHexString
+import me.zhanghai.android.files.util.valueCompat
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Future
+
+class ChecksumInfoLiveData(path: Path) : PathObserverLiveData>(path) {
+ private var future: Future? = null
+
+ init {
+ loadValue()
+ observe()
+ }
+
+ override fun loadValue() {
+ future?.cancel(true)
+ value = Loading(value?.value)
+ future = (AsyncTask.THREAD_POOL_EXECUTOR as ExecutorService).submit {
+ val value = try {
+ val messageDigests =
+ ChecksumInfo.Algorithm.entries.associateWith { it.createMessageDigest() }
+ path.newInputStream().use { inputStream ->
+ val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+ while (true) {
+ val readSize = inputStream.read(buffer)
+ if (readSize == -1) {
+ break
+ }
+ messageDigests.values.forEach { it.update(buffer, 0, readSize) }
+ }
+ }
+ val checksumInfo = ChecksumInfo(
+ messageDigests.mapValues { it.value.digest().toHexString() }
+ )
+ Success(checksumInfo)
+ } catch (e: Exception) {
+ Failure(valueCompat.value, e)
+ }
+ postValue(value)
+ }
+ }
+
+ override fun close() {
+ super.close()
+
+ future?.cancel(true)
+ }
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/Crc32MessageDigest.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/Crc32MessageDigest.kt
new file mode 100644
index 000000000..919facc7d
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/Crc32MessageDigest.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2024 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.fileproperties.checksum
+
+import java.security.MessageDigest
+import java.util.zip.CRC32
+
+class Crc32MessageDigest : MessageDigest("CRC32") {
+ private val crc32 = CRC32()
+
+ override fun engineUpdate(input: Byte) {
+ crc32.update(input.toInt())
+ }
+
+ override fun engineUpdate(input: ByteArray, offset: Int, length: Int) {
+ crc32.update(input, offset, length)
+ }
+
+ override fun engineDigest(): ByteArray {
+ val value = crc32.value
+ crc32.reset()
+ return ByteArray(4).apply {
+ this[0] = (value ushr 24).toByte()
+ this[1] = (value ushr 16).toByte()
+ this[2] = (value ushr 8).toByte()
+ this[3] = value.toByte()
+ }
+ }
+
+ override fun engineReset() {
+ crc32.reset()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/FilePropertiesChecksumTabFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/FilePropertiesChecksumTabFragment.kt
new file mode 100644
index 000000000..33d2de721
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/FilePropertiesChecksumTabFragment.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2024 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.fileproperties.checksum
+
+import androidx.core.widget.doAfterTextChanged
+import java8.nio.file.Path
+import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.WriteWith
+import me.zhanghai.android.files.R
+import me.zhanghai.android.files.databinding.FilePropertiesChecksumCompareItemBinding
+import me.zhanghai.android.files.file.FileItem
+import me.zhanghai.android.files.fileproperties.FilePropertiesTabFragment
+import me.zhanghai.android.files.util.ParcelableArgs
+import me.zhanghai.android.files.util.ParcelableParceler
+import me.zhanghai.android.files.util.Stateful
+import me.zhanghai.android.files.util.args
+import me.zhanghai.android.files.util.layoutInflater
+import me.zhanghai.android.files.util.viewModels
+
+class FilePropertiesChecksumTabFragment : FilePropertiesTabFragment() {
+ private val args by args()
+
+ private val viewModel by viewModels { { FilePropertiesChecksumTabViewModel(args.path) } }
+
+ override fun onResume() {
+ super.onResume()
+
+ viewModel.checksumInfoLiveData.observe(viewLifecycleOwner) { onChecksumInfoChanged(it) }
+ }
+
+ override fun refresh() {
+ viewModel.reload()
+ }
+
+ private fun onChecksumInfoChanged(stateful: Stateful) {
+ bindView(stateful) { checksumInfo ->
+ checksumInfo.checksums.forEach { addItemView(it.key.nameRes, it.value) }
+ addCompareEdit(checksumInfo)
+ }
+ }
+
+ private fun ViewBuilder.addCompareEdit(checksumInfo: ChecksumInfo) {
+ val binding = getScrapItemBinding(FilePropertiesChecksumCompareItemBinding::class.java)
+ ?.also { addView(it) }
+ ?: FilePropertiesChecksumCompareItemBinding.inflate(
+ linearLayout.context.layoutInflater, linearLayout, true
+ )
+ .also { it.root.tag = it }
+ binding.compareEdit.doAfterTextChanged { editable ->
+ val text = editable!!.toString().trim()
+ if (text.isEmpty()) {
+ binding.compareLayout.helperText = null
+ binding.compareLayout.error = null
+ return@doAfterTextChanged
+ }
+ val matchingAlgorithm = checksumInfo.checksums.firstNotNullOfOrNull {
+ if (it.value.equals(text, true)) it.key else null
+ }
+ if (matchingAlgorithm != null) {
+ binding.compareLayout.helperText =
+ getString(
+ R.string.file_properties_checksum_compare_match_format,
+ getString(matchingAlgorithm.nameRes)
+ )
+ return@doAfterTextChanged
+ }
+ val prefixMatchingAlgorithm = checksumInfo.checksums.firstNotNullOfOrNull {
+ if (it.value.startsWith(text, true)) it.key else null
+ }
+ if (prefixMatchingAlgorithm != null) {
+ binding.compareLayout.helperText =
+ getString(
+ R.string.file_properties_checksum_compare_prefix_match_format,
+ getString(prefixMatchingAlgorithm.nameRes)
+ )
+ return@doAfterTextChanged
+ }
+ binding.compareLayout.error =
+ getString(R.string.file_properties_checksum_compare_no_match)
+ }
+ }
+
+ companion object {
+ fun isAvailable(file: FileItem): Boolean = file.attributes.isRegularFile
+ }
+
+ @Parcelize
+ class Args(val path: @WriteWith Path) : ParcelableArgs
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/FilePropertiesChecksumTabViewModel.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/FilePropertiesChecksumTabViewModel.kt
new file mode 100644
index 000000000..98844fcb3
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/checksum/FilePropertiesChecksumTabViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2024 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.fileproperties.checksum
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import java8.nio.file.Path
+import me.zhanghai.android.files.util.Stateful
+
+class FilePropertiesChecksumTabViewModel(path: Path) : ViewModel() {
+ private val _checksumInfoLiveData = ChecksumInfoLiveData(path)
+ val checksumInfoLiveData: LiveData>
+ get() = _checksumInfoLiveData
+
+ fun reload() {
+ _checksumInfoLiveData.loadValue()
+ }
+
+ override fun onCleared() {
+ _checksumInfoLiveData.close()
+ }
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/image/ExifInterfaceExtensions.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/image/ExifInterfaceExtensions.kt
index 7310a1f2c..fc235f083 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/image/ExifInterfaceExtensions.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/image/ExifInterfaceExtensions.kt
@@ -7,12 +7,12 @@ package me.zhanghai.android.files.fileproperties.image
import android.annotation.SuppressLint
import androidx.exifinterface.media.ExifInterface
-import me.zhanghai.android.files.util.takeIfNotBlank
-import org.threeten.bp.Duration
-import org.threeten.bp.Instant
-import org.threeten.bp.ZoneId
-import org.threeten.bp.ZoneOffset
+import java.time.Duration
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZoneOffset
import kotlin.math.roundToLong
+import me.zhanghai.android.files.util.takeIfNotBlank
fun ExifInterface.getAttributeNotBlank(tag: String): String? =
getAttribute(tag)?.takeIfNotBlank()
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/image/FilePropertiesImageTabFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/image/FilePropertiesImageTabFragment.kt
index a3275ffcd..78bcc00e9 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/image/FilePropertiesImageTabFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/image/FilePropertiesImageTabFragment.kt
@@ -7,7 +7,6 @@ package me.zhanghai.android.files.fileproperties.image
import android.content.Intent
import android.location.Geocoder
-import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import java8.nio.file.Path
import kotlinx.coroutines.Job
@@ -44,8 +43,8 @@ class FilePropertiesImageTabFragment : FilePropertiesTabFragment() {
private var addressJob: Job? = null
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
+ override fun onResume() {
+ super.onResume()
viewModel.imageInfoLiveData.observe(viewLifecycleOwner) { onImageInfoChanged(it) }
}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/image/ImageInfo.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/image/ImageInfo.kt
index 6905fe27b..bb01a8983 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/image/ImageInfo.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/image/ImageInfo.kt
@@ -6,7 +6,7 @@
package me.zhanghai.android.files.fileproperties.image
import android.util.Size
-import org.threeten.bp.Instant
+import java.time.Instant
class ImageInfo(
val dimensions: Size?,
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/FilePropertiesPermissionsTabFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/FilePropertiesPermissionTabFragment.kt
similarity index 79%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/FilePropertiesPermissionsTabFragment.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/FilePropertiesPermissionTabFragment.kt
index f91c158a0..ab35c0d60 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/FilePropertiesPermissionsTabFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/FilePropertiesPermissionTabFragment.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import android.os.Bundle
import me.zhanghai.android.files.R
@@ -17,7 +17,7 @@ import me.zhanghai.android.files.provider.common.toModeString
import me.zhanghai.android.files.util.Stateful
import me.zhanghai.android.files.util.viewModels
-class FilePropertiesPermissionsTabFragment : FilePropertiesTabFragment() {
+class FilePropertiesPermissionTabFragment : FilePropertiesTabFragment() {
private val viewModel by viewModels({ requireParentFragment() })
override fun onActivityCreated(savedInstanceState: Bundle?) {
@@ -35,27 +35,27 @@ class FilePropertiesPermissionsTabFragment : FilePropertiesTabFragment() {
val attributes = file.attributes as PosixFileAttributes
val owner = attributes.owner()
addItemView(
- R.string.file_properties_permissions_owner, getPrincipalText(owner), owner?.let {
- { SetOwnerDialogFragment.show(file, this@FilePropertiesPermissionsTabFragment) }
+ R.string.file_properties_permission_owner, getPrincipalText(owner), owner?.let {
+ { SetOwnerDialogFragment.show(file, this@FilePropertiesPermissionTabFragment) }
}
)
val group = attributes.group()
addItemView(
- R.string.file_properties_permissions_group, getPrincipalText(group), group?.let {
- { SetGroupDialogFragment.show(file, this@FilePropertiesPermissionsTabFragment) }
+ R.string.file_properties_permission_group, getPrincipalText(group), group?.let {
+ { SetGroupDialogFragment.show(file, this@FilePropertiesPermissionTabFragment) }
}
)
val mode = attributes.mode()
addItemView(
- R.string.file_properties_permissions_mode, if (mode != null) {
+ R.string.file_properties_permission_mode, if (mode != null) {
getString(
- R.string.file_properties_permissions_mode_format, mode.toModeString(),
+ R.string.file_properties_permission_mode_format, mode.toModeString(),
mode.toInt()
)
} else {
getString(R.string.unknown)
}, if (mode != null && !attributes.isSymbolicLink) {
- { SetModeDialogFragment.show(file, this@FilePropertiesPermissionsTabFragment) }
+ { SetModeDialogFragment.show(file, this@FilePropertiesPermissionTabFragment) }
} else {
null
}
@@ -63,7 +63,7 @@ class FilePropertiesPermissionsTabFragment : FilePropertiesTabFragment() {
val seLinuxContext = attributes.seLinuxContext()
if (seLinuxContext != null) {
addItemView(
- R.string.file_properties_permissions_selinux_context,
+ R.string.file_properties_permission_selinux_context,
if (seLinuxContext.isNotEmpty()) {
seLinuxContext.toString()
} else {
@@ -71,7 +71,7 @@ class FilePropertiesPermissionsTabFragment : FilePropertiesTabFragment() {
}
) {
SetSeLinuxContextDialogFragment.show(
- file, this@FilePropertiesPermissionsTabFragment
+ file, this@FilePropertiesPermissionTabFragment
)
}
}
@@ -82,7 +82,7 @@ class FilePropertiesPermissionsTabFragment : FilePropertiesTabFragment() {
if (principal != null) {
if (principal.name != null) {
getString(
- R.string.file_properties_permissions_principal_format, principal.name,
+ R.string.file_properties_permission_principal_format, principal.name,
principal.id
)
} else {
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/GroupListAdapter.kt
similarity index 87%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListAdapter.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/GroupListAdapter.kt
index 46cd2cc00..e649dd8aa 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListAdapter.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/GroupListAdapter.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import androidx.annotation.DrawableRes
import me.zhanghai.android.files.R
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/permission/GroupListLiveData.kt
similarity index 84%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListLiveData.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/GroupListLiveData.kt
index c4398620b..b054e0b0e 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/GroupListLiveData.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/GroupListLiveData.kt
@@ -3,11 +3,11 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
-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/ModeBitListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/ModeBitListAdapter.kt
similarity index 95%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/ModeBitListAdapter.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/ModeBitListAdapter.kt
index 042e5eecd..d2951c660 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/ModeBitListAdapter.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/ModeBitListAdapter.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import android.view.View
import android.view.ViewGroup
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalItem.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/PrincipalItem.kt
similarity index 82%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalItem.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/PrincipalItem.kt
index b838a663c..7aa67d8dc 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalItem.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/PrincipalItem.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import android.content.pm.ApplicationInfo
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/permission/PrincipalListAdapter.kt
similarity index 87%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListAdapter.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/PrincipalListAdapter.kt
index c4d2da179..69c037fe2 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListAdapter.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/PrincipalListAdapter.kt
@@ -3,13 +3,13 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import android.view.ViewGroup
import androidx.annotation.DrawableRes
import androidx.recyclerview.widget.RecyclerView
-import coil.clear
-import coil.loadAny
+import coil.dispose
+import coil.load
import me.zhanghai.android.files.R
import me.zhanghai.android.files.coil.ignoreError
import me.zhanghai.android.files.compat.getDrawableCompat
@@ -46,24 +46,24 @@ 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) {
binding.principalText.context.getString(
- R.string.file_properties_permissions_principal_format, principal.name, principal.id
+ R.string.file_properties_permission_principal_format, principal.name, principal.id
)
} else {
principal.id.toString()
}
binding.labelText.text = principal.applicationLabels.firstOrNull()
?: binding.labelText.resources.getString(
- R.string.file_properties_permissions_set_principal_system
+ R.string.file_properties_permission_set_principal_system
)
}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListLiveData.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/PrincipalListLiveData.kt
similarity index 97%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListLiveData.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/PrincipalListLiveData.kt
index d5573f303..3ea336bc4 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/PrincipalListLiveData.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/PrincipalListLiveData.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import android.content.pm.ApplicationInfo
import android.os.AsyncTask
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetGroupDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetGroupDialogFragment.kt
similarity index 95%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetGroupDialogFragment.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetGroupDialogFragment.kt
index 30b126178..61c3a170a 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetGroupDialogFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetGroupDialogFragment.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
@@ -23,7 +23,7 @@ class SetGroupDialogFragment : SetPrincipalDialogFragment() {
override val viewModel: SetPrincipalViewModel by viewModels { { SetGroupViewModel() } }
@StringRes
- override val titleRes: Int = R.string.file_properties_permissions_set_group_title
+ override val titleRes: Int = R.string.file_properties_permission_set_group_title
override fun createAdapter(selectionLiveData: SelectionLiveData): PrincipalListAdapter =
GroupListAdapter(selectionLiveData)
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetGroupViewModel.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetGroupViewModel.kt
similarity index 73%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetGroupViewModel.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetGroupViewModel.kt
index 8c5dd0034..bb5d420c2 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetGroupViewModel.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetGroupViewModel.kt
@@ -3,6 +3,6 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
class SetGroupViewModel : SetPrincipalViewModel(GroupListLiveData())
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetModeDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetModeDialogFragment.kt
similarity index 94%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetModeDialogFragment.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetModeDialogFragment.kt
index ea04d7497..8b77472ac 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetModeDialogFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetModeDialogFragment.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import android.app.Dialog
import android.os.Bundle
@@ -43,16 +43,16 @@ class SetModeDialogFragment : AppCompatDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
MaterialAlertDialogBuilder(requireContext(), theme)
- .setTitle(R.string.file_properties_permissions_set_mode_title)
+ .setTitle(R.string.file_properties_permission_set_mode_title)
.apply {
binding = SetModeDialogBinding.inflate(context.layoutInflater)
binding.ownerText.setOnClickListener { binding.ownerDropDown.show() }
val isDirectory = args.file.attributes.isDirectory
normalModeBitNames = getStringArray(
if (isDirectory) {
- R.array.file_properties_permissions_set_mode_normal_mode_bits_directory
+ R.array.file_properties_permission_set_mode_normal_mode_bits_directory
} else {
- R.array.file_properties_permissions_set_mode_normal_mode_bits_file
+ R.array.file_properties_permission_set_mode_normal_mode_bits_file
}
)
ownerAdapter = ModeBitListAdapter(OWNER_MODE_BITS, normalModeBitNames)
@@ -74,7 +74,7 @@ class SetModeDialogFragment : AppCompatDialogFragment() {
}
binding.specialText.setOnClickListener { binding.specialDropDown.show() }
specialModeBitNames = getStringArray(
- R.array.file_properties_permissions_set_mode_special_mode_bits
+ R.array.file_properties_permission_set_mode_special_mode_bits
)
specialAdapter = ModeBitListAdapter(SPECIAL_MODE_BITS, specialModeBitNames)
binding.specialDropDown.setAdapter(specialAdapter)
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetModeViewModel.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetModeViewModel.kt
similarity index 94%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetModeViewModel.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetModeViewModel.kt
index 2a5d30165..5d48b5652 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetModeViewModel.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetModeViewModel.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetOwnerDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetOwnerDialogFragment.kt
similarity index 95%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetOwnerDialogFragment.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetOwnerDialogFragment.kt
index 14aee0192..bed5d1c8b 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetOwnerDialogFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetOwnerDialogFragment.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
@@ -24,7 +24,7 @@ class SetOwnerDialogFragment : SetPrincipalDialogFragment() {
override val viewModel: SetPrincipalViewModel by viewModels { { SetOwnerViewModel() } }
@StringRes
- override val titleRes: Int = R.string.file_properties_permissions_set_owner_title
+ override val titleRes: Int = R.string.file_properties_permission_set_owner_title
override fun createAdapter(selectionLiveData: SelectionLiveData): PrincipalListAdapter =
UserListAdapter(selectionLiveData)
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetOwnerViewModel.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetOwnerViewModel.kt
similarity index 73%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetOwnerViewModel.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetOwnerViewModel.kt
index c25d11ca5..1555d5efa 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetOwnerViewModel.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetOwnerViewModel.kt
@@ -3,6 +3,6 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
class SetOwnerViewModel : SetPrincipalViewModel(UserListLiveData())
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/permission/SetPrincipalDialogFragment.kt
similarity index 98%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetPrincipalDialogFragment.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetPrincipalDialogFragment.kt
index 4d7c3be11..7ad3b6729 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetPrincipalDialogFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetPrincipalDialogFragment.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import android.app.Dialog
import android.os.Bundle
@@ -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/fileproperties/permissions/SetPrincipalViewModel.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetPrincipalViewModel.kt
similarity index 97%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetPrincipalViewModel.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetPrincipalViewModel.kt
index b4718e79d..da891cf4d 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetPrincipalViewModel.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetPrincipalViewModel.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetSeLinuxContextDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetSeLinuxContextDialogFragment.kt
similarity index 91%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetSeLinuxContextDialogFragment.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetSeLinuxContextDialogFragment.kt
index 56f6ed170..086d1e4b4 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/SetSeLinuxContextDialogFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/SetSeLinuxContextDialogFragment.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import android.app.Dialog
import android.os.Bundle
@@ -31,7 +31,7 @@ class SetSeLinuxContextDialogFragment : AppCompatDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
MaterialAlertDialogBuilder(requireContext(), theme)
- .setTitle(R.string.file_properties_permissions_set_selinux_context_title)
+ .setTitle(R.string.file_properties_permission_set_selinux_context_title)
.apply {
binding = SetSelinuxContextDialogBinding.inflate(context.layoutInflater)
if (savedInstanceState == null) {
@@ -43,7 +43,7 @@ class SetSeLinuxContextDialogFragment : AppCompatDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> setSeLinuxContext() }
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(
- R.string.file_properties_permissions_set_selinux_context_restore
+ R.string.file_properties_permission_set_selinux_context_restore
) { _, _ -> restoreSeLinuxContext() }
.create()
.apply {
@@ -66,7 +66,7 @@ class SetSeLinuxContextDialogFragment : AppCompatDialogFragment() {
private val argsSeLinuxContext: String
get() {
val attributes = args.file.attributes as PosixFileAttributes
- return attributes.seLinuxContext()?.toString() ?: ""
+ return attributes.seLinuxContext()?.toString().orEmpty()
}
private fun restoreSeLinuxContext() {
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/UserListAdapter.kt
similarity index 87%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListAdapter.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/UserListAdapter.kt
index a62dc58ea..825344378 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListAdapter.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/UserListAdapter.kt
@@ -3,7 +3,7 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
import androidx.annotation.DrawableRes
import me.zhanghai.android.files.R
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/permission/UserListLiveData.kt
similarity index 81%
rename from app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListLiveData.kt
rename to app/src/main/java/me/zhanghai/android/files/fileproperties/permission/UserListLiveData.kt
index 67a18f898..a201b3bb7 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/permissions/UserListLiveData.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/permission/UserListLiveData.kt
@@ -3,11 +3,11 @@
* All Rights Reserved.
*/
-package me.zhanghai.android.files.fileproperties.permissions
+package me.zhanghai.android.files.fileproperties.permission
-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/fileproperties/video/FilePropertiesVideoTabFragment.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/video/FilePropertiesVideoTabFragment.kt
index bd46db520..6118f0cd2 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/video/FilePropertiesVideoTabFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/video/FilePropertiesVideoTabFragment.kt
@@ -7,7 +7,6 @@ package me.zhanghai.android.files.fileproperties.video
import android.content.Intent
import android.location.Geocoder
-import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import java8.nio.file.Path
import kotlinx.coroutines.Job
@@ -41,8 +40,8 @@ class FilePropertiesVideoTabFragment : FilePropertiesTabFragment() {
private var addressJob: Job? = null
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
+ override fun onResume() {
+ super.onResume()
viewModel.videoInfoLiveData.observe(viewLifecycleOwner) { onVideoInfoChanged(it) }
}
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/video/VideoInfo.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/video/VideoInfo.kt
index 662cec234..726661fbd 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/video/VideoInfo.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/video/VideoInfo.kt
@@ -6,8 +6,8 @@
package me.zhanghai.android.files.fileproperties.video
import android.util.Size
-import org.threeten.bp.Duration
-import org.threeten.bp.Instant
+import java.time.Duration
+import java.time.Instant
// @see com.android.providers.media.scan.ModernMediaScanner.scanItemVideo
// @see com.android.documentsui.inspector.MediaView.showVideoData
diff --git a/app/src/main/java/me/zhanghai/android/files/fileproperties/video/VideoInfoLiveData.kt b/app/src/main/java/me/zhanghai/android/files/fileproperties/video/VideoInfoLiveData.kt
index 1cae4f2eb..a62511e7e 100644
--- a/app/src/main/java/me/zhanghai/android/files/fileproperties/video/VideoInfoLiveData.kt
+++ b/app/src/main/java/me/zhanghai/android/files/fileproperties/video/VideoInfoLiveData.kt
@@ -8,6 +8,7 @@ package me.zhanghai.android.files.fileproperties.video
import android.media.MediaMetadataRetriever
import android.os.AsyncTask
import android.util.Size
+import java.time.Duration
import java8.nio.file.Path
import me.zhanghai.android.files.compat.use
import me.zhanghai.android.files.fileproperties.PathObserverLiveData
@@ -20,7 +21,6 @@ import me.zhanghai.android.files.util.Stateful
import me.zhanghai.android.files.util.Success
import me.zhanghai.android.files.util.setDataSource
import me.zhanghai.android.files.util.valueCompat
-import org.threeten.bp.Duration
class VideoInfoLiveData(path: Path) : PathObserverLiveData>(path) {
init {
diff --git a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerAddTilePreference.kt b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerAddTilePreference.kt
new file mode 100644
index 000000000..f41a9be02
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerAddTilePreference.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.ftpserver
+
+import android.app.StatusBarManager
+import android.content.Context
+import android.os.Build
+import android.util.AttributeSet
+import androidx.annotation.AttrRes
+import androidx.annotation.RequiresApi
+import androidx.annotation.StyleRes
+import androidx.preference.Preference
+import me.zhanghai.android.files.R
+import me.zhanghai.android.files.compat.getSystemServiceCompat
+import me.zhanghai.android.files.compat.mainExecutorCompat
+import me.zhanghai.android.files.util.requestAddTileService
+import me.zhanghai.android.files.util.showToast
+
+class FtpServerAddTilePreference : Preference {
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : super(
+ context, attrs, defStyleAttr
+ )
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ @AttrRes defStyleAttr: Int,
+ @StyleRes defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ init {
+ isPersistent = false
+ }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ override fun onClick() {
+ val statusBarManager = context.getSystemServiceCompat(StatusBarManager::class.java)
+ statusBarManager.requestAddTileService(
+ FtpServerTileService::class.java, context.mainExecutorCompat
+ ) { result ->
+ val resultRes = when (result) {
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED ->
+ return@requestAddTileService
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED ->
+ R.string.ftp_server_add_tile_result_already_added
+ StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED ->
+ R.string.ftp_server_add_tile_result_added
+ else -> R.string.ftp_server_add_tile_result_error
+ }
+ context.showToast(resultRes)
+ }
+ }
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerServiceNotification.kt b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerNotification.kt
similarity index 73%
rename from app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerServiceNotification.kt
rename to app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerNotification.kt
index 11030c1de..b358ec580 100644
--- a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerServiceNotification.kt
+++ b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerNotification.kt
@@ -10,13 +10,15 @@ import android.app.Service
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.ServiceCompat
import me.zhanghai.android.files.R
import me.zhanghai.android.files.app.NotificationIds
+import me.zhanghai.android.files.compat.stopForegroundCompat
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",
@@ -28,14 +30,23 @@ val ftpServerServiceNotificationTemplate: NotificationTemplate =
colorRes = R.color.color_primary,
smallIcon = R.drawable.notification_icon,
contentTitleRes = R.string.ftp_server_notification_title,
- contentTextRes = R.string.ftp_server_notification_text,
ongoing = true,
+ onlyAlertOnce = true,
category = NotificationCompat.CATEGORY_SERVICE,
priority = NotificationCompat.PRIORITY_LOW
)
-object FtpServerServiceNotification {
- fun startForeground(service: Service) {
+class FtpServerNotification(private val service: Service) {
+ private val receiver = FtpServerUrl.createChangeReceiver(service) { doStartForeground() }
+
+ fun startForeground() {
+ doStartForeground()
+ receiver.register()
+ }
+
+ private fun doStartForeground() {
+ val contextText = FtpServerUrl.getUrl()
+ ?: service.getString(R.string.ftp_server_notification_text_no_local_inet_address)
val contentIntent = FtpServerActivity::class.createIntent()
var pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@@ -49,16 +60,17 @@ object FtpServerServiceNotification {
service, FtpServerReceiver::class.hashCode(), stopIntent, pendingIntentFlags
)
val notification = ftpServerServiceNotificationTemplate.createBuilder(service)
+ .setContentText(contextText)
.setContentIntent(contentPendingIntent)
.addAction(
- R.drawable.stop_icon_white_24dp, service.getString(R.string.stop),
- stopPendingIntent
+ R.drawable.stop_icon_white_24dp, service.getString(R.string.stop), stopPendingIntent
)
.build()
service.startForeground(NotificationIds.FTP_SERVER, notification)
}
- fun stopForeground(service: Service) {
- service.stopForeground(true)
+ fun stopForeground() {
+ receiver.unregister()
+ service.stopForegroundCompat(ServiceCompat.STOP_FOREGROUND_REMOVE)
}
}
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 62339129f..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,9 @@ class FtpServerService : Service() {
_stateLiveData.value = value
}
- private lateinit var wakeLock: FtpServerWakeLock
+ private lateinit var wakeWifiLock: WakeWifiLock
+
+ private lateinit var notification: FtpServerNotification
private val executorService = Executors.newSingleThreadExecutor()
@@ -35,7 +38,8 @@ class FtpServerService : Service() {
override fun onCreate() {
super.onCreate()
- wakeLock = FtpServerWakeLock()
+ wakeWifiLock = WakeWifiLock(FtpServerService::class.java.simpleName)
+ notification = FtpServerNotification(this)
executeStart()
}
@@ -54,8 +58,8 @@ class FtpServerService : Service() {
if (state == State.STARTING || state == State.RUNNING) {
return
}
- wakeLock.acquire()
- FtpServerServiceNotification.startForeground(this)
+ wakeWifiLock.isAcquired = true
+ notification.startForeground()
state = State.STARTING
executorService.execute { doStart() }
}
@@ -63,8 +67,8 @@ class FtpServerService : Service() {
private fun onStartError(exception: Exception) {
state = State.STOPPED
showToast(exception.toString())
- FtpServerServiceNotification.stopForeground(this)
- wakeLock.release()
+ notification.stopForeground()
+ wakeWifiLock.isAcquired = false
stopSelf()
}
@@ -74,8 +78,8 @@ class FtpServerService : Service() {
}
state = State.STOPPING
executorService.execute { doStop() }
- FtpServerServiceNotification.stopForeground(this)
- wakeLock.release()
+ notification.stopForeground()
+ wakeWifiLock.isAcquired = false
}
@WorkerThread
diff --git a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerTileService.kt b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerTileService.kt
index 7ee30d78a..145f1d1c4 100644
--- a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerTileService.kt
+++ b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerTileService.kt
@@ -10,6 +10,7 @@ import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import androidx.lifecycle.Observer
+import me.zhanghai.android.files.compat.doWithStartForegroundServiceAllowed
@RequiresApi(Build.VERSION_CODES.N)
class FtpServerTileService : TileService() {
@@ -30,8 +31,8 @@ class FtpServerTileService : TileService() {
private fun onFtpServerStateChanged(state: FtpServerService.State) {
val tile = qsTile
when (state) {
- FtpServerService.State.STARTING, FtpServerService.State.RUNNING ->
- tile.state = Tile.STATE_ACTIVE
+ FtpServerService.State.STARTING,
+ FtpServerService.State.RUNNING -> tile.state = Tile.STATE_ACTIVE
FtpServerService.State.STOPPING -> tile.state = Tile.STATE_UNAVAILABLE
FtpServerService.State.STOPPED -> tile.state = Tile.STATE_INACTIVE
}
@@ -49,6 +50,6 @@ class FtpServerTileService : TileService() {
}
private fun toggle() {
- FtpServerService.toggle(this)
+ doWithStartForegroundServiceAllowed { FtpServerService.toggle(this) }
}
}
diff --git a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerUrl.kt b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerUrl.kt
new file mode 100644
index 000000000..f465b9a9f
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerUrl.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.ftpserver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import me.zhanghai.android.files.settings.Settings
+import me.zhanghai.android.files.util.RuntimeBroadcastReceiver
+import me.zhanghai.android.files.util.getLocalAddress
+import me.zhanghai.android.files.util.valueCompat
+import java.net.InetAddress
+
+object FtpServerUrl {
+ fun getUrl(): String? {
+ val localAddress = InetAddress::class.getLocalAddress() ?: return null
+ val username = if (!Settings.FTP_SERVER_ANONYMOUS_LOGIN.valueCompat) {
+ Settings.FTP_SERVER_USERNAME.valueCompat
+ } else {
+ null
+ }
+ val host = localAddress.hostAddress
+ val port = Settings.FTP_SERVER_PORT.valueCompat
+ return "ftp://${if (username != null) "$username@" else ""}$host:$port/"
+ }
+
+ fun createChangeReceiver(context: Context, onChange: () -> Unit): RuntimeBroadcastReceiver =
+ RuntimeBroadcastReceiver(
+ IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION), object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ onChange()
+ }
+ }, context
+ )
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerUrlPreference.kt b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerUrlPreference.kt
index 9fe80cdf3..71c41eaa9 100644
--- a/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerUrlPreference.kt
+++ b/app/src/main/java/me/zhanghai/android/files/ftpserver/FtpServerUrlPreference.kt
@@ -5,11 +5,7 @@
package me.zhanghai.android.files.ftpserver
-import android.content.BroadcastReceiver
import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.net.ConnectivityManager
import android.util.AttributeSet
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
@@ -25,16 +21,13 @@ import me.zhanghai.android.files.R
import me.zhanghai.android.files.app.clipboardManager
import me.zhanghai.android.files.settings.Settings
import me.zhanghai.android.files.util.copyText
-import me.zhanghai.android.files.util.getLocalAddress
import me.zhanghai.android.files.util.valueCompat
-import java.net.InetAddress
class FtpServerUrlPreference : Preference {
- private val observer = Observer { updateSummary() }
- private val connectivityReceiver = ConnectivityReceiver()
+ private val observer = Observer { updateUrl() }
+ private val receiver = FtpServerUrl.createChangeReceiver(context) { updateUrl() }
- private val contextMenuListener = ContextMenuListener()
- private var hasUrl = false
+ private var url: String? = null
constructor(context: Context) : super(context)
@@ -53,7 +46,7 @@ class FtpServerUrlPreference : Preference {
init {
isPersistent = false
- updateSummary()
+ updateUrl()
}
override fun onAttached() {
@@ -62,9 +55,7 @@ class FtpServerUrlPreference : Preference {
Settings.FTP_SERVER_ANONYMOUS_LOGIN.observeForever(observer)
Settings.FTP_SERVER_USERNAME.observeForever(observer)
Settings.FTP_SERVER_PORT.observeForever(observer)
- context.registerReceiver(
- connectivityReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
- )
+ receiver.register()
}
override fun onDetached() {
@@ -73,76 +64,45 @@ class FtpServerUrlPreference : Preference {
Settings.FTP_SERVER_ANONYMOUS_LOGIN.removeObserver(observer)
Settings.FTP_SERVER_USERNAME.removeObserver(observer)
Settings.FTP_SERVER_PORT.removeObserver(observer)
- context.unregisterReceiver(connectivityReceiver)
+ receiver.unregister()
}
- private fun updateSummary() {
- val localAddress = InetAddress::class.getLocalAddress()
- val summary: String
- if (localAddress != null) {
- val username = if (!Settings.FTP_SERVER_ANONYMOUS_LOGIN.valueCompat) {
- Settings.FTP_SERVER_USERNAME.valueCompat
- } else {
- null
- }
- val host = localAddress.hostAddress
- val port = Settings.FTP_SERVER_PORT.valueCompat
- summary = "ftp://${if (username != null) "$username@" else ""}$host:$port/"
- hasUrl = true
- } else {
- summary = context.getString(R.string.ftp_server_url_summary_no_local_inet_address)
- hasUrl = false
- }
- setSummary(summary)
+ private fun updateUrl() {
+ url = FtpServerUrl.getUrl()
+ summary = url ?: context.getString(R.string.ftp_server_url_summary_no_local_inet_address)
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
- holder.itemView.setOnCreateContextMenuListener(contextMenuListener)
- }
-
- private inner class ConnectivityReceiver : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- when (val action = intent.action) {
- ConnectivityManager.CONNECTIVITY_ACTION -> updateSummary()
- else -> throw IllegalArgumentException(action)
- }
- }
- }
-
- private inner class ContextMenuListener : OnCreateContextMenuListener {
- override fun onCreateContextMenu(
- menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?
- ) {
- if (!hasUrl) {
- return
- }
- val url = summary!!
- menu
- .setHeaderTitle(url)
- .apply {
+ holder.itemView.setOnCreateContextMenuListener(object : OnCreateContextMenuListener {
+ override fun onCreateContextMenu(
+ menu: ContextMenu,
+ view: View,
+ menuInfo: ContextMenuInfo?
+ ) {
+ val url = url ?: return
+ menu.apply {
+ setHeaderTitle(url)
add(Menu.NONE, Menu.NONE, Menu.NONE, R.string.ftp_server_url_menu_copy_url)
.setOnMenuItemClickListener {
clipboardManager.copyText(url, context)
true
}
- }
- .apply {
if (!Settings.FTP_SERVER_ANONYMOUS_LOGIN.valueCompat) {
val password = Settings.FTP_SERVER_PASSWORD.valueCompat
if (password.isNotEmpty()) {
add(
Menu.NONE, Menu.NONE, Menu.NONE,
R.string.ftp_server_url_menu_copy_password
- )
- .setOnMenuItemClickListener {
- clipboardManager.copyText(password, context)
- true
- }
+ ).setOnMenuItemClickListener {
+ clipboardManager.copyText(password, context)
+ true
+ }
}
}
}
- }
+ }
+ })
}
}
diff --git a/app/src/main/java/me/zhanghai/android/files/navigation/EditBookmarkDirectoryDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/navigation/EditBookmarkDirectoryDialogFragment.kt
index d14929423..e91a93958 100644
--- a/app/src/main/java/me/zhanghai/android/files/navigation/EditBookmarkDirectoryDialogFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/navigation/EditBookmarkDirectoryDialogFragment.kt
@@ -30,9 +30,8 @@ import me.zhanghai.android.files.util.putState
import me.zhanghai.android.files.util.setTextWithSelection
class EditBookmarkDirectoryDialogFragment : AppCompatDialogFragment() {
- private val pickPathLauncher = registerForActivityResult(
- FileListActivity.PickDirectoryContract(), this::onPickPathResult
- )
+ private val openPathLauncher =
+ registerForActivityResult(FileListActivity.OpenDirectoryContract(), ::onOpenPathResult)
private val args by args()
@@ -75,10 +74,10 @@ class EditBookmarkDirectoryDialogFragment : AppCompatDialogFragment() {
}
private fun onEditPath() {
- pickPathLauncher.launchSafe(path, this)
+ openPathLauncher.launchSafe(path, this)
}
- private fun onPickPathResult(result: Path?) {
+ private fun onOpenPathResult(result: Path?) {
result ?: return
path = result
updatePathText()
diff --git a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationFragment.kt b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationFragment.kt
index 4b2b78737..897189d23 100644
--- a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationFragment.kt
+++ b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationFragment.kt
@@ -5,6 +5,7 @@
package me.zhanghai.android.files.navigation
+import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -14,16 +15,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.LinearLayoutManager
import java8.nio.file.Path
import me.zhanghai.android.files.databinding.NavigationFragmentBinding
-import me.zhanghai.android.files.file.DocumentTreeUri
-import me.zhanghai.android.files.file.asDocumentTreeUri
-import me.zhanghai.android.files.file.releasePersistablePermission
-import me.zhanghai.android.files.provider.document.documentTreeUri
-import me.zhanghai.android.files.provider.document.isDocumentPath
-import me.zhanghai.android.files.settings.StandardDirectoryListActivity
-import me.zhanghai.android.files.storage.AddStorageDialogActivity
-import me.zhanghai.android.files.storage.Storage
-import me.zhanghai.android.files.util.createIntent
-import me.zhanghai.android.files.util.putArgs
import me.zhanghai.android.files.util.startActivitySafe
class NavigationFragment : Fragment(), NavigationItem.Listener {
@@ -47,7 +38,7 @@ class NavigationFragment : Fragment(), NavigationItem.Listener {
binding.recyclerView.setHasFixedSize(true)
// TODO: Needed?
- //binding.recyclerView.setItemAnimator(new NoChangeAnimationItemAnimator());
+ //binding.recyclerView.setItemAnimator(new NoChangeAnimationItemAnimator())
val context = requireContext()
binding.recyclerView.layoutManager = LinearLayoutManager(context)
adapter = NavigationListAdapter(this, context)
@@ -77,34 +68,8 @@ class NavigationFragment : Fragment(), NavigationItem.Listener {
listener.navigateToRoot(path)
}
- override fun onAddStorage() {
- startActivitySafe(AddStorageDialogActivity::class.createIntent())
- }
-
- override fun onEditStorage(storage: Storage) {
- startActivitySafe(storage.createEditIntent())
- }
-
- // TODO
- // FIXME: Navigate away on async storage removal
- fun removeDocumentTree(treeUri: DocumentTreeUri) {
- treeUri.releasePersistablePermission()
- val currentPath = listener.currentPath
- if (currentPath.isDocumentPath
- && currentPath.documentTreeUri.asDocumentTreeUri() == treeUri) {
- listener.navigateToDefaultRoot()
- }
- }
-
- override fun onEditStandardDirectory(standardDirectory: StandardDirectory) {
- startActivitySafe(StandardDirectoryListActivity::class.createIntent())
- }
-
- override fun onEditBookmarkDirectory(bookmarkDirectory: BookmarkDirectory) {
- startActivitySafe(
- EditBookmarkDirectoryDialogActivity::class.createIntent()
- .putArgs(EditBookmarkDirectoryDialogFragment.Args(bookmarkDirectory))
- )
+ override fun launchIntent(intent: Intent) {
+ startActivitySafe(intent)
}
override fun closeNavigationDrawer() {
diff --git a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationItem.kt b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationItem.kt
index 1f88ae2a0..48def26ee 100644
--- a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationItem.kt
+++ b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationItem.kt
@@ -11,7 +11,6 @@ import android.graphics.drawable.Drawable
import androidx.annotation.DrawableRes
import java8.nio.file.Path
import me.zhanghai.android.files.compat.getDrawableCompat
-import me.zhanghai.android.files.storage.Storage
abstract class NavigationItem {
abstract val id: Long
@@ -35,11 +34,7 @@ abstract class NavigationItem {
val currentPath: Path
fun navigateTo(path: Path)
fun navigateToRoot(path: Path)
- fun onAddStorage()
- fun onEditStorage(storage: Storage)
- fun onEditStandardDirectory(standardDirectory: StandardDirectory)
- fun onEditBookmarkDirectory(bookmarkDirectory: BookmarkDirectory)
+ fun launchIntent(intent: Intent)
fun closeNavigationDrawer()
- fun startActivity(intent: Intent)
}
}
diff --git a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationItems.kt b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationItems.kt
index 3d0995584..f9851b69c 100644
--- a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationItems.kt
+++ b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationItems.kt
@@ -25,18 +25,22 @@ import me.zhanghai.android.files.file.asFileSize
import me.zhanghai.android.files.ftpserver.FtpServerActivity
import me.zhanghai.android.files.settings.Settings
import me.zhanghai.android.files.settings.SettingsActivity
+import me.zhanghai.android.files.settings.StandardDirectoryListActivity
+import me.zhanghai.android.files.storage.AddStorageDialogActivity
import me.zhanghai.android.files.storage.FileSystemRoot
import me.zhanghai.android.files.storage.Storage
import me.zhanghai.android.files.storage.StorageVolumeListLiveData
import me.zhanghai.android.files.util.createIntent
import me.zhanghai.android.files.util.isMounted
+import me.zhanghai.android.files.util.putArgs
+import me.zhanghai.android.files.util.supportsExternalStorageManager
import me.zhanghai.android.files.util.valueCompat
val navigationItems: List
get() =
mutableListOf().apply {
addAll(storageItems)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ if (Environment::class.supportsExternalStorageManager()) {
// Starting with R, we can get read/write access to non-primary storage volumes with
// MANAGE_EXTERNAL_STORAGE. However before R, we only have read-only access to them
// and need to use the Storage Access Framework instead, so hide them in this case
@@ -60,7 +64,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 +82,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 +102,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 +196,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 +224,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 +345,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 +356,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 +377,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 +386,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/navigation/NavigationListAdapter.kt b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationListAdapter.kt
index 93aa34a1e..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
@@ -40,27 +40,38 @@ class NavigationListAdapter(
) : SimpleAdapter() {
@SuppressLint("PrivateResource", "RestrictedApi")
private val viewAttributes = context.obtainStyledAttributesCompat(
- null, R.styleable.NavigationView, R.attr.navigationViewStyle,
- R.style.Widget_MaterialComponents_NavigationView
+ null, com.google.android.material.R.styleable.NavigationView,
+ com.google.android.material.R.attr.navigationViewStyle,
+ com.google.android.material.R.style.Widget_MaterialComponents_NavigationView
).use { a ->
- val itemShapeAppearance = a.getResourceId(R.styleable.NavigationView_itemShapeAppearance, 0)
+ val itemShapeAppearance = a.getResourceId(
+ com.google.android.material.R.styleable.NavigationView_itemShapeAppearance, 0
+ )
val itemShapeAppearanceOverlay = a.getResourceId(
- R.styleable.NavigationView_itemShapeAppearanceOverlay, 0
+ com.google.android.material.R.styleable.NavigationView_itemShapeAppearanceOverlay, 0
+ )
+ val itemShapeFillColor = a.getColorStateList(
+ com.google.android.material.R.styleable.NavigationView_itemShapeFillColor
+ )
+ val itemShapeInsetStart = a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_itemShapeInsetStart, 0
+ )
+ val itemShapeInsetEnd = a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_itemShapeInsetEnd, 0
+ )
+ val itemShapeInsetTop = a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_itemShapeInsetTop, 0
+ )
+ val itemShapeInsetBottom = a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_itemShapeInsetBottom, 0
)
- val itemShapeFillColor = a.getColorStateList(R.styleable.NavigationView_itemShapeFillColor)
- val itemShapeInsetStart =
- a.getDimensionPixelSize(R.styleable.NavigationView_itemShapeInsetStart, 0)
- val itemShapeInsetEnd =
- a.getDimensionPixelSize(R.styleable.NavigationView_itemShapeInsetEnd, 0)
- val itemShapeInsetTop =
- a.getDimensionPixelSize(R.styleable.NavigationView_itemShapeInsetTop, 0)
- val itemShapeInsetBottom =
- a.getDimensionPixelSize(R.styleable.NavigationView_itemShapeInsetBottom, 0)
val itemBackground = createItemShapeDrawable(
itemShapeAppearance, itemShapeAppearanceOverlay, itemShapeFillColor,
itemShapeInsetStart, itemShapeInsetEnd, itemShapeInsetTop, itemShapeInsetBottom, context
)
- val controlHighlightColor = context.getColorStateListByAttr(R.attr.colorControlHighlight)
+ val controlHighlightColor = context.getColorStateListByAttr(
+ com.google.android.material.R.attr.colorControlHighlight
+ )
val itemForegroundMaskFillColor = ColorStateList.valueOf(Color.WHITE)
val itemForegroundMask = createItemShapeDrawable(
itemShapeAppearance, itemShapeAppearanceOverlay, itemForegroundMaskFillColor,
@@ -68,28 +79,46 @@ class NavigationListAdapter(
)
val itemForeground = RippleDrawable(controlHighlightColor, null, itemForegroundMask)
context.obtainStyledAttributesCompat(
- null, R.styleable.NavigationViewExtra, R.attr.navigationViewStyle, 0
+ null, R.styleable.NavigationViewExtra,
+ com.google.android.material.R.attr.navigationViewStyle, 0
).use { a2 ->
ViewAttributes(
- a.getDimensionPixelSize(R.styleable.NavigationView_itemHorizontalPadding, 0),
- a.getDimensionPixelSize(R.styleable.NavigationView_itemVerticalPadding, 0),
+ a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_itemHorizontalPadding, 0
+ ),
+ a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_itemVerticalPadding, 0
+ ),
itemBackground,
itemForeground,
- a.getDimensionPixelSize(R.styleable.NavigationView_itemIconSize, 0),
- a.getColorStateList(R.styleable.NavigationView_itemIconTint),
- a.getDimensionPixelSize(R.styleable.NavigationView_itemIconPadding, 0),
+ a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_itemIconSize, 0
+ ),
+ a.getColorStateList(
+ com.google.android.material.R.styleable.NavigationView_itemIconTint
+ ),
+ a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_itemIconPadding, 0
+ ),
a.getResourceId(
- R.styleable.NavigationView_itemTextAppearance, ResourcesCompat.ID_NULL
+ com.google.android.material.R.styleable.NavigationView_itemTextAppearance,
+ ResourcesCompat.ID_NULL
+ ),
+ a.getColorStateList(
+ com.google.android.material.R.styleable.NavigationView_itemTextColor
),
- a.getColorStateList(R.styleable.NavigationView_itemTextColor),
a2.getResourceId(
R.styleable.NavigationViewExtra_itemSubtitleTextAppearance,
ResourcesCompat.ID_NULL
),
a2.getColorStateList(R.styleable.NavigationViewExtra_itemSubtitleTextColor),
a2.getDimension(R.styleable.NavigationViewExtra_itemSubtitleTextSize, 0f),
- a.getDimensionPixelSize(R.styleable.NavigationView_dividerInsetStart, 0),
- a.getDimensionPixelSize(R.styleable.NavigationView_dividerInsetEnd, 0),
+ a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_dividerInsetStart, 0
+ ),
+ a.getDimensionPixelSize(
+ com.google.android.material.R.styleable.NavigationView_dividerInsetEnd, 0
+ ),
a2.getDimensionPixelSize(R.styleable.NavigationViewExtra_dividerVerticalPadding, 0)
)
}
@@ -131,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)
@@ -193,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/navigation/NavigationRecyclerView.kt b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationRecyclerView.kt
index e793c4df5..3b5c59b67 100644
--- a/app/src/main/java/me/zhanghai/android/files/navigation/NavigationRecyclerView.kt
+++ b/app/src/main/java/me/zhanghai/android/files/navigation/NavigationRecyclerView.kt
@@ -23,9 +23,11 @@ import me.zhanghai.android.files.util.getDrawableByAttr
import me.zhanghai.android.files.util.isLayoutDirectionRtl
class NavigationRecyclerView : RecyclerView {
- private val verticalPadding =
- context.getDimensionPixelSize(R.dimen.design_navigation_padding_bottom)
- private val actionBarSize = context.getDimensionPixelSizeByAttr(R.attr.actionBarSize)
+ private val verticalPadding = context.getDimensionPixelSize(
+ com.google.android.material.R.dimen.design_navigation_padding_bottom
+ )
+ private val actionBarSize =
+ context.getDimensionPixelSizeByAttr(androidx.appcompat.R.attr.actionBarSize)
private val maxWidth = context.getDimensionPixelSize(R.dimen.navigation_max_width)
private var scrim = context.getDrawableByAttr(android.R.attr.statusBarColor)
diff --git a/app/src/main/java/me/zhanghai/android/files/nonfree/CrashlyticsInitializer.kt b/app/src/main/java/me/zhanghai/android/files/nonfree/CrashlyticsInitializer.kt
index e613f5eb6..d2dc3a4e0 100644
--- a/app/src/main/java/me/zhanghai/android/files/nonfree/CrashlyticsInitializer.kt
+++ b/app/src/main/java/me/zhanghai/android/files/nonfree/CrashlyticsInitializer.kt
@@ -9,12 +9,12 @@ import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.content.pm.Signature
import com.google.firebase.crashlytics.FirebaseCrashlytics
+import java.security.MessageDigest
+import java.security.NoSuchAlgorithmException
import me.zhanghai.android.files.BuildConfig
import me.zhanghai.android.files.app.application
import me.zhanghai.android.files.app.packageManager
import me.zhanghai.android.files.util.getPackageInfoOrNull
-import java.security.MessageDigest
-import java.security.NoSuchAlgorithmException
object CrashlyticsInitializer {
private val HEX_CHARS = "0123456789ABCDEF".toCharArray()
@@ -39,9 +39,10 @@ object CrashlyticsInitializer {
val packageInfo = packageManager.getPackageInfoOrNull(
application.packageName, PackageManager.GET_SIGNATURES
) ?: return false
- return packageInfo.signatures.size == 1
- && computeCertificateFingerprint(packageInfo.signatures[0]) == ("87:3B:9B:60:C7:7C:F7"
- + ":F3:CD:5F:AE:66:D0:FE:11:2C:4A:86:97:3E:11:8E:E8:A2:9C:34:6C:4C:67:3C:97:F0")
+ val signatures = packageInfo.signatures ?: return false
+ return signatures.size == 1 &&
+ computeCertificateFingerprint(signatures[0]) == "87:3B:9B:60:C7:7C:F7:F3:CD:5F:AE:66" +
+ ":D0:FE:11:2C:4A:86:97:3E:11:8E:E8:A2:9C:34:6C:4C:67:3C:97:F0"
}
private fun computeCertificateFingerprint(certificate: Signature): String {
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/FileSystemProviders.kt b/app/src/main/java/me/zhanghai/android/files/provider/FileSystemProviders.kt
index bde790c95..7126ca270 100644
--- a/app/src/main/java/me/zhanghai/android/files/provider/FileSystemProviders.kt
+++ b/app/src/main/java/me/zhanghai/android/files/provider/FileSystemProviders.kt
@@ -19,6 +19,8 @@ import me.zhanghai.android.files.provider.linux.LinuxFileSystemProvider
import me.zhanghai.android.files.provider.root.isRunningAsRoot
import me.zhanghai.android.files.provider.sftp.SftpFileSystemProvider
import me.zhanghai.android.files.provider.smb.SmbFileSystemProvider
+import me.zhanghai.android.files.provider.webdav.WebDavFileSystemProvider
+import me.zhanghai.android.files.provider.webdav.WebDavsFileSystemProvider
object FileSystemProviders {
/**
@@ -42,6 +44,8 @@ object FileSystemProviders {
FileSystemProvider.installProvider(FtpesFileSystemProvider)
FileSystemProvider.installProvider(SftpFileSystemProvider)
FileSystemProvider.installProvider(SmbFileSystemProvider)
+ FileSystemProvider.installProvider(WebDavFileSystemProvider)
+ FileSystemProvider.installProvider(WebDavsFileSystemProvider)
}
Files.installFileTypeDetector(AndroidFileTypeDetector)
}
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..f9613dd50
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveExceptionExtensions.kt
@@ -0,0 +1,91 @@
+/*
+ * 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: 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, message)
+ else -> FileSystemException(file.toString(), null, 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)
+ }
+
+ @Throws(IOException::class)
+ override fun read(b: ByteArray): Int =
+ try {
+ super.read(b)
+ } catch (e: ArchiveException) {
+ throw e.toFileSystemOrInterruptedIOException(file)
+ }
+
+ @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)
+ }
+
+ @Throws(IOException::class)
+ override fun skip(n: Long): Long = try {
+ super.skip(n)
+ } catch (e: ArchiveException) {
+ throw e.toFileSystemOrInterruptedIOException(file)
+ }
+
+ @Throws(IOException::class)
+ override fun available(): Int =
+ try {
+ super.available()
+ } catch (e: ArchiveException) {
+ throw e.toFileSystemOrInterruptedIOException(file)
+ }
+
+ @Throws(IOException::class)
+ override fun close() {
+ try {
+ super.close()
+ } catch (e: ArchiveException) {
+ throw e.toFileSystemOrInterruptedIOException(file)
+ }
+ }
+
+ @Throws(IOException::class)
+ override fun reset() {
+ try {
+ super.reset()
+ } catch (e: ArchiveException) {
+ throw e.toFileSystemOrInterruptedIOException(file)
+ }
+ }
+}
diff --git a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributeView.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributeView.kt
index d9566a24a..683146881 100644
--- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributeView.kt
+++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileAttributeView.kt
@@ -1,37 +1,60 @@
/*
- * Copyright (c) 2018 Hai Zhang
+ * Copyright (c) 2019 Hai Zhang
* All Rights Reserved.
*/
package me.zhanghai.android.files.provider.archive
-import android.os.Parcel
-import android.os.Parcelable
-import me.zhanghai.android.files.provider.root.RootablePosixFileAttributeView
-import me.zhanghai.android.files.util.readParcelable
+import java8.nio.file.Path
+import java8.nio.file.attribute.FileTime
+import me.zhanghai.android.files.provider.common.ByteString
+import me.zhanghai.android.files.provider.common.PosixFileAttributeView
+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 java.io.IOException
+
+internal class ArchiveFileAttributeView(private val path: Path) : PosixFileAttributeView {
+ override fun name(): String = NAME
+
+ @Throws(IOException::class)
+ override fun readAttributes(): ArchiveFileAttributes {
+ val fileSystem = path.fileSystem as ArchiveFileSystem
+ val entry = fileSystem.getEntry(path)
+ return ArchiveFileAttributes.from(fileSystem.archiveFile, entry)
+ }
-internal class ArchiveFileAttributeView(
- private val path: ArchivePath
-) : RootablePosixFileAttributeView(
- path, LocalArchiveFileAttributeView(path), { RootArchiveFileAttributeView(it, path) }
-) {
- private constructor(source: Parcel) : this(source.readParcelable()!!)
+ override fun setTimes(
+ lastModifiedTime: FileTime?,
+ lastAccessTime: FileTime?,
+ createTime: FileTime?
+ ) {
+ throw UnsupportedOperationException()
+ }
- override fun describeContents(): Int = 0
+ override fun setOwner(owner: PosixUser) {
+ throw UnsupportedOperationException()
+ }
- override fun writeToParcel(dest: Parcel, flags: Int) {
- dest.writeParcelable(path as Parcelable, flags)
+ override fun setGroup(group: PosixGroup) {
+ throw UnsupportedOperationException()
}
- companion object {
- val SUPPORTED_NAMES = LocalArchiveFileAttributeView.SUPPORTED_NAMES
+ override fun setMode(mode: Set) {
+ throw UnsupportedOperationException()
+ }
- @JvmField
- val CREATOR = object : Parcelable.Creator {
- override fun createFromParcel(source: Parcel): ArchiveFileAttributeView =
- ArchiveFileAttributeView(source)
+ override fun setSeLinuxContext(context: ByteString) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun restoreSeLinuxContext() {
+ throw UnsupportedOperationException()
+ }
+
+ companion object {
+ private val NAME = ArchiveFileSystemProvider.scheme
- override fun newArray(size: Int): Array = arrayOfNulls(size)
- }
+ val SUPPORTED_NAMES = setOf("basic", "posix", NAME)
}
}
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..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
@@ -10,22 +10,15 @@ 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.EncryptedFileAttributes
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(
@@ -39,94 +32,31 @@ 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 {
- 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 isEncrypted = entry.isEncrypted
val entryName = entry.name
return ArchiveFileAttributes(
lastModifiedTime, lastAccessTime, creationTime, type, size, fileKey, owner, group,
- mode, seLinuxContext, entryName
+ mode, seLinuxContext, isEncrypted, 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/ArchiveFileStore.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileStore.kt
index a7ca58b47..71a8fb0e2 100644
--- a/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileStore.kt
+++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchiveFileStore.kt
@@ -1,36 +1,42 @@
/*
- * Copyright (c) 2018 Hai Zhang
+ * Copyright (c) 2019 Hai Zhang
* All Rights Reserved.
*/
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.root.RootPosixFileStore
-import me.zhanghai.android.files.provider.root.RootablePosixFileStore
+import java8.nio.file.attribute.FileAttributeView
+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 java.io.IOException
-internal class ArchiveFileStore(private val archiveFile: Path) : RootablePosixFileStore(
- archiveFile, LocalArchiveFileStore(archiveFile), { RootPosixFileStore(it) }
-) {
- private constructor(source: Parcel) : this(
- source.readParcelable(Path::class.java.classLoader) as Path
- )
+internal class ArchiveFileStore(private val archiveFile: Path) : PosixFileStore() {
+ override fun refresh() {}
- override fun describeContents(): Int = 0
+ override fun name(): String = archiveFile.toString()
- override fun writeToParcel(dest: Parcel, flags: Int) {
- dest.writeParcelable(archiveFile as Parcelable, flags)
- }
+ override fun type(): String = MimeType.guessFromPath(archiveFile.toString()).value
- companion object {
- @JvmField
- val CREATOR = object : Parcelable.Creator {
- override fun createFromParcel(source: Parcel): ArchiveFileStore =
- ArchiveFileStore(source)
+ override fun isReadOnly(): Boolean = true
- override fun newArray(size: Int): Array = arrayOfNulls(size)
- }
+ @Throws(IOException::class)
+ override fun setReadOnly(readOnly: Boolean) {
+ throw UnsupportedOperationException()
}
+
+ @Throws(IOException::class)
+ override fun getTotalSpace(): Long = archiveFile.size()
+
+ override fun getUsableSpace(): Long = 0
+
+ override fun getUnallocatedSpace(): Long = 0
+
+ override fun supportsFileAttributeView(type: Class): Boolean =
+ ArchiveFileSystemProvider.supportsFileAttributeView(type)
+
+ override fun supportsFileAttributeView(name: String): Boolean =
+ name in ArchiveFileAttributeView.SUPPORTED_NAMES
}
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..c43903828 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
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018 Hai Zhang
+ * Copyright (c) 2019 Hai Zhang
* All Rights Reserved.
*/
@@ -7,62 +7,215 @@ package me.zhanghai.android.files.provider.archive
import android.os.Parcel
import android.os.Parcelable
+import java8.nio.file.ClosedFileSystemException
+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
+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.remote.RemoteFileSystemException
-import me.zhanghai.android.files.provider.root.RootableFileSystem
-import org.apache.commons.compress.archivers.ArchiveEntry
+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
internal class ArchiveFileSystem(
- provider: ArchiveFileSystemProvider,
- archiveFile: Path
-) : RootableFileSystem(
- { LocalArchiveFileSystem(it as ArchiveFileSystem, provider, archiveFile) },
- { RootArchiveFileSystem(it) }
-), ByteStringListPathCreator {
- override val localFileSystem: LocalArchiveFileSystem
- get() = super.localFileSystem as LocalArchiveFileSystem
-
- override val rootFileSystem: RootArchiveFileSystem
- get() = super.rootFileSystem as RootArchiveFileSystem
+ private val provider: ArchiveFileSystemProvider,
+ val archiveFile: Path
+) : FileSystem(), ByteStringListPathCreator, Parcelable {
+ val rootDirectory = ArchivePath(this, SEPARATOR_BYTE_STRING)
- val rootDirectory: ArchivePath
- get() = localFileSystem.rootDirectory
+ init {
+ if (!rootDirectory.isAbsolute) {
+ throw AssertionError("Root directory $rootDirectory must be absolute")
+ }
+ if (rootDirectory.nameCount != 0) {
+ throw AssertionError("Root directory $rootDirectory must contain no names")
+ }
+ }
val defaultDirectory: ArchivePath
- get() = localFileSystem.defaultDirectory
+ get() = rootDirectory
- val archiveFile: Path
- get() = localFileSystem.archiveFile
+ private val lock = Any()
+
+ private var isOpen = true
+
+ private var passwords = listOf()
+
+ private var isRefreshNeeded = true
+
+ private var entries: Map? = null
+
+ private var tree: Map>? = null
@Throws(IOException::class)
- fun getEntryAsLocal(path: Path): ArchiveEntry = localFileSystem.getEntry(path)
+ fun getEntry(path: Path): ReadArchive.Entry =
+ synchronized(lock) {
+ ensureEntriesLocked(path)
+ getEntryLocked(path)
+ }
@Throws(IOException::class)
- fun newInputStreamAsLocal(file: Path): InputStream = localFileSystem.newInputStream(file)
+ private fun getEntryLocked(path: Path): ReadArchive.Entry =
+ synchronized(lock) {
+ entries!![path] ?: throw NoSuchFileException(path.toString())
+ }
@Throws(IOException::class)
- fun getDirectoryChildrenAsLocal(directory: Path): List =
- localFileSystem.getDirectoryChildren(directory)
+ fun newInputStream(file: Path): InputStream =
+ synchronized(lock) {
+ ensureEntriesLocked(file)
+ val entry = getEntryLocked(file)
+ if (entry.isDirectory) {
+ throw IsDirectoryException(file.toString())
+ }
+ val inputStream = try {
+ ArchiveReader.newInputStream(archiveFile, passwords, entry)
+ } catch (e: ArchiveException) {
+ throw e.toFileSystemOrInterruptedIOException(file)
+ } ?: throw NoSuchFileException(file.toString())
+ ArchiveExceptionInputStream(inputStream, file)
+ }
@Throws(IOException::class)
- fun readSymbolicLinkAsLocal(link: Path): String = localFileSystem.readSymbolicLink(link)
+ fun getDirectoryChildren(directory: Path): List =
+ synchronized(lock) {
+ ensureEntriesLocked(directory)
+ val entry = getEntryLocked(directory)
+ if (!entry.isDirectory) {
+ throw NotDirectoryException(directory.toString())
+ }
+ tree!![directory]!!
+ }
+
+ @Throws(IOException::class)
+ fun readSymbolicLink(link: Path): String =
+ synchronized(lock) {
+ ensureEntriesLocked(link)
+ val entry = getEntryLocked(link)
+ if (!entry.isSymbolicLink) {
+ throw NotLinkException(link.toString())
+ }
+ entry.symbolicLinkTarget.orEmpty()
+ }
+
+ fun addPassword(password: String) {
+ synchronized(lock) {
+ if (!isOpen) {
+ throw ClosedFileSystemException()
+ }
+ passwords += password
+ }
+ }
fun refresh() {
- localFileSystem.refresh()
- rootFileSystem.refresh()
+ synchronized(lock) {
+ if (!isOpen) {
+ throw ClosedFileSystemException()
+ }
+ isRefreshNeeded = true
+ }
+ }
+
+ @Throws(IOException::class)
+ private fun ensureEntriesLocked(file: Path) {
+ if (!isOpen) {
+ throw ClosedFileSystemException()
+ }
+ if (isRefreshNeeded) {
+ val entriesAndTree = try {
+ ArchiveReader.readEntries(archiveFile, passwords, rootDirectory)
+ } catch (e: ArchiveException) {
+ throw e.toFileSystemOrInterruptedIOException(file)
+ }
+ entries = entriesAndTree.first
+ tree = entriesAndTree.second
+ isRefreshNeeded = false
+ }
+ }
+
+ override fun provider(): FileSystemProvider = provider
+
+ override fun close() {
+ synchronized(lock) {
+ if (!isOpen) {
+ return
+ }
+ provider.removeFileSystem(this)
+ isRefreshNeeded = false
+ entries = null
+ tree = null
+ isOpen = false
+ }
+ }
+
+ override fun isOpen(): Boolean = synchronized(lock) { isOpen }
+
+ override fun isReadOnly(): Boolean = true
+
+ override fun getSeparator(): String = SEPARATOR_STRING
+
+ override fun getRootDirectories(): Iterable = listOf(rootDirectory)
+
+ override fun getFileStores(): Iterable {
+ // TODO
+ throw UnsupportedOperationException()
}
- @Throws(RemoteFileSystemException::class)
- fun doRefreshIfNeededAsRoot() {
- rootFileSystem.doRefreshIfNeeded()
+ override fun supportedFileAttributeViews(): Set =
+ ArchiveFileAttributeView.SUPPORTED_NAMES
+
+ override fun getPath(first: String, vararg more: String): ArchivePath {
+ val path = ByteStringBuilder(first.toByteString())
+ .apply { more.forEach { append(SEPARATOR).append(it.toByteString()) } }
+ .toByteString()
+ return ArchivePath(this, path)
+ }
+
+ override fun getPath(first: ByteString, vararg more: ByteString): ArchivePath {
+ val path = ByteStringBuilder(first)
+ .apply { more.forEach { append(SEPARATOR).append(it) } }
+ .toByteString()
+ return ArchivePath(this, path)
+ }
+
+ override fun getPathMatcher(syntaxAndPattern: String): PathMatcher {
+ throw UnsupportedOperationException()
+ }
+
+ override fun getUserPrincipalLookupService(): UserPrincipalLookupService {
+ throw UnsupportedOperationException()
+ }
+
+ @Throws(IOException::class)
+ override fun newWatchService(): WatchService {
+ // TODO
+ throw UnsupportedOperationException()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+ if (javaClass != other?.javaClass) {
+ return false
+ }
+ other as ArchiveFileSystem
+ return archiveFile == other.archiveFile
}
- override fun getPath(first: ByteString, vararg more: ByteString): ArchivePath =
- localFileSystem.getPath(first, *more)
+ override fun hashCode(): Int = archiveFile.hashCode()
override fun describeContents(): Int = 0
@@ -71,7 +224,9 @@ internal class ArchiveFileSystem(
}
companion object {
- const val SEPARATOR: Byte = LocalArchiveFileSystem.SEPARATOR
+ const val SEPARATOR = '/'.code.toByte()
+ private val SEPARATOR_BYTE_STRING = SEPARATOR.toByteString()
+ private const val SEPARATOR_STRING = SEPARATOR.toInt().toChar().toString()
@JvmField
val CREATOR = object : Parcelable.Creator {
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..972e367dc 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
@@ -1,35 +1,293 @@
/*
- * Copyright (c) 2018 Hai Zhang
+ * Copyright (c) 2019 Hai Zhang
* All Rights Reserved.
*/
package me.zhanghai.android.files.provider.archive
+import java8.nio.channels.FileChannel
+import java8.nio.channels.SeekableByteChannel
+import java8.nio.file.AccessDeniedException
+import java8.nio.file.AccessMode
+import java8.nio.file.CopyOption
+import java8.nio.file.DirectoryStream
+import java8.nio.file.FileStore
+import java8.nio.file.FileSystem
+import java8.nio.file.LinkOption
+import java8.nio.file.OpenOption
import java8.nio.file.Path
+import java8.nio.file.Paths
+import java8.nio.file.ProviderMismatchException
+import java8.nio.file.attribute.BasicFileAttributes
+import java8.nio.file.attribute.FileAttribute
import java8.nio.file.attribute.FileAttributeView
-import me.zhanghai.android.files.provider.root.RootableFileSystemProvider
+import java8.nio.file.spi.FileSystemProvider
+import me.zhanghai.android.files.provider.common.ByteStringPath
+import me.zhanghai.android.files.provider.common.FileSystemCache
+import me.zhanghai.android.files.provider.common.PathListDirectoryStream
+import me.zhanghai.android.files.provider.common.PathObservable
+import me.zhanghai.android.files.provider.common.PathObservableProvider
+import me.zhanghai.android.files.provider.common.ReadOnlyFileSystemException
+import me.zhanghai.android.files.provider.common.Searchable
+import me.zhanghai.android.files.provider.common.WalkFileTreeSearchable
+import me.zhanghai.android.files.provider.common.decodedPathByteString
+import me.zhanghai.android.files.provider.common.decodedQueryByteString
+import me.zhanghai.android.files.provider.common.isSameFile
+import me.zhanghai.android.files.provider.common.toAccessModes
+import me.zhanghai.android.files.provider.common.toByteString
+import me.zhanghai.android.files.provider.common.toOpenOptions
+import java.io.IOException
+import java.io.InputStream
+import java.net.URI
-object ArchiveFileSystemProvider : RootableFileSystemProvider(
- { LocalArchiveFileSystemProvider(it as ArchiveFileSystemProvider) },
- { RootArchiveFileSystemProvider(LocalArchiveFileSystemProvider.SCHEME) }
-) {
- override val localProvider: LocalArchiveFileSystemProvider
- get() = super.localProvider as LocalArchiveFileSystemProvider
+object ArchiveFileSystemProvider : FileSystemProvider(), PathObservableProvider, Searchable {
+ private const val SCHEME = "archive"
- override val rootProvider: RootArchiveFileSystemProvider
- get() = super.rootProvider as RootArchiveFileSystemProvider
+ private val fileSystems = FileSystemCache()
+
+ override fun getScheme(): String = SCHEME
+
+ override fun newFileSystem(uri: URI, env: Map): FileSystem {
+ uri.requireSameScheme()
+ val archiveFile = uri.archiveFile
+ return fileSystems.create(archiveFile) { newFileSystem(archiveFile) }
+ }
+
+ override fun newFileSystem(file: Path, env: Map): FileSystem = newFileSystem(file)
internal fun getOrNewFileSystem(archiveFile: Path): ArchiveFileSystem =
- localProvider.getOrNewFileSystem(archiveFile)
+ fileSystems.getOrCreate(archiveFile) { newFileSystem(archiveFile) }
+
+ private fun newFileSystem(archiveFile: Path): ArchiveFileSystem =
+ ArchiveFileSystem(this, archiveFile)
+
+ override fun getFileSystem(uri: URI): FileSystem {
+ uri.requireSameScheme()
+ val archiveFile = uri.archiveFile
+ return fileSystems[archiveFile]
+ }
internal fun removeFileSystem(fileSystem: ArchiveFileSystem) {
- localProvider.removeFileSystem(fileSystem)
+ fileSystems.remove(fileSystem.archiveFile, fileSystem)
+ }
+
+ override fun getPath(uri: URI): Path {
+ uri.requireSameScheme()
+ val archiveFile = uri.archiveFile
+ val path = uri.decodedQueryByteString
+ ?: throw IllegalArgumentException("URI must have a query")
+ return getOrNewFileSystem(archiveFile).getPath(path)
+ }
+
+ private fun URI.requireSameScheme() {
+ val scheme = scheme
+ require(scheme == SCHEME) { "URI scheme $scheme must be $SCHEME" }
+ }
+
+ private val URI.archiveFile: Path
+ get() {
+ val path = decodedPathByteString
+ ?: throw IllegalArgumentException("URI must have a path")
+ // Drop the first character which is always a slash.
+ val archiveUri = URI.create(path.toString().drop(1))
+ return Paths.get(archiveUri)
+ }
+
+ @Throws(IOException::class)
+ override fun newInputStream(file: Path, vararg options: OpenOption): InputStream {
+ file as? ArchivePath ?: throw ProviderMismatchException(file.toString())
+ options.toOpenOptions().checkForArchive()
+ return file.fileSystem.newInputStream(file)
+ }
+
+ override fun newFileChannel(
+ file: Path,
+ options: Set,
+ vararg attributes: FileAttribute<*>
+ ): FileChannel {
+ file as? ArchivePath ?: throw ProviderMismatchException(file.toString())
+ options.toOpenOptions().checkForArchive()
+ if (attributes.isNotEmpty()) {
+ throw UnsupportedOperationException(attributes.contentToString())
+ }
+ throw UnsupportedOperationException()
+ }
+
+ override fun newByteChannel(
+ file: Path,
+ options: Set,
+ vararg attributes: FileAttribute<*>
+ ): SeekableByteChannel {
+ file as? ArchivePath ?: throw ProviderMismatchException(file.toString())
+ options.toOpenOptions().checkForArchive()
+ if (attributes.isNotEmpty()) {
+ throw UnsupportedOperationException(attributes.contentToString())
+ }
+ throw UnsupportedOperationException()
+ }
+
+ @Throws(IOException::class)
+ override fun newDirectoryStream(
+ directory: Path,
+ filter: DirectoryStream.Filter
+ ): DirectoryStream {
+ directory as? ArchivePath ?: throw ProviderMismatchException(directory.toString())
+ val children = directory.fileSystem.getDirectoryChildren(directory)
+ return PathListDirectoryStream(children, filter)
+ }
+
+ @Throws(IOException::class)
+ override fun createDirectory(directory: Path, vararg attributes: FileAttribute<*>) {
+ directory as? ArchivePath ?: throw ProviderMismatchException(directory.toString())
+ throw ReadOnlyFileSystemException(directory.toString())
+ }
+
+ @Throws(IOException::class)
+ override fun createSymbolicLink(link: Path, target: Path, vararg attributes: FileAttribute<*>) {
+ link as? ArchivePath ?: throw ProviderMismatchException(link.toString())
+ when (target) {
+ is ArchivePath, is ByteStringPath -> {}
+ else -> throw ProviderMismatchException(target.toString())
+ }
+ throw ReadOnlyFileSystemException(link.toString(), target.toString(), null)
+ }
+
+ @Throws(IOException::class)
+ override fun createLink(link: Path, existing: Path) {
+ link as? ArchivePath ?: throw ProviderMismatchException(link.toString())
+ existing as? ArchivePath ?: throw ProviderMismatchException(existing.toString())
+ throw ReadOnlyFileSystemException(link.toString(), existing.toString(), null)
+ }
+
+ @Throws(IOException::class)
+ override fun delete(path: Path) {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ throw ReadOnlyFileSystemException(path.toString())
+ }
+
+ @Throws(IOException::class)
+ override fun readSymbolicLink(link: Path): Path {
+ link as? ArchivePath ?: throw ProviderMismatchException(link.toString())
+ val target = link.fileSystem.readSymbolicLink(link)
+ return ByteStringPath(target.toByteString())
+ }
+
+ @Throws(IOException::class)
+ override fun copy(source: Path, target: Path, vararg options: CopyOption) {
+ source as? ArchivePath ?: throw ProviderMismatchException(source.toString())
+ target as? ArchivePath ?: throw ProviderMismatchException(target.toString())
+ throw ReadOnlyFileSystemException(source.toString(), target.toString(), null)
+ }
+
+ @Throws(IOException::class)
+ override fun move(source: Path, target: Path, vararg options: CopyOption) {
+ source as? ArchivePath ?: throw ProviderMismatchException(source.toString())
+ target as? ArchivePath ?: throw ProviderMismatchException(target.toString())
+ throw ReadOnlyFileSystemException(source.toString(), target.toString(), null)
+ }
+
+ @Throws(IOException::class)
+ override fun isSameFile(path: Path, path2: Path): Boolean {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ if (path == path2) {
+ return true
+ }
+ if (path2 !is ArchivePath) {
+ return false
+ }
+ val fileSystem = path.fileSystem
+ if (!fileSystem.archiveFile.isSameFile(path2.fileSystem.archiveFile)) {
+ return false
+ }
+ return path == fileSystem.getPath(path2.toString())
+ }
+
+ override fun isHidden(path: Path): Boolean {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ return false
+ }
+
+ override fun getFileStore(path: Path): FileStore {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ val archiveFile = path.fileSystem.archiveFile
+ return ArchiveFileStore(archiveFile)
+ }
+
+ @Throws(IOException::class)
+ override fun checkAccess(path: Path, vararg modes: AccessMode) {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ val accessModes = modes.toAccessModes()
+ path.fileSystem.getEntry(path)
+ if (accessModes.write || accessModes.execute) {
+ throw AccessDeniedException(path.toString())
+ }
+ }
+
+ override fun getFileAttributeView(
+ path: Path,
+ type: Class,
+ vararg options: LinkOption
+ ): V? {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ if (!supportsFileAttributeView(type)) {
+ return null
+ }
+ @Suppress("UNCHECKED_CAST")
+ return getFileAttributeView(path) as V
}
internal fun supportsFileAttributeView(type: Class): Boolean =
- LocalArchiveFileSystemProvider.supportsFileAttributeView(type)
+ type.isAssignableFrom(ArchiveFileAttributeView::class.java)
+
+ @Throws(IOException::class)
+ override fun readAttributes(
+ path: Path,
+ type: Class,
+ vararg options: LinkOption
+ ): A {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ if (!type.isAssignableFrom(ArchiveFileAttributes::class.java)) {
+ throw UnsupportedOperationException(type.toString())
+ }
+ @Suppress("UNCHECKED_CAST")
+ return getFileAttributeView(path).readAttributes() as A
+ }
+
+ private fun getFileAttributeView(path: ArchivePath): ArchiveFileAttributeView =
+ ArchiveFileAttributeView(path)
+
+ override fun readAttributes(
+ path: Path,
+ attributes: String,
+ vararg options: LinkOption
+ ): Map {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ throw UnsupportedOperationException()
+ }
+
+ override fun setAttribute(
+ path: Path,
+ attribute: String,
+ value: Any,
+ vararg options: LinkOption
+ ) {
+ path as? ArchivePath ?: throw ProviderMismatchException(path.toString())
+ throw UnsupportedOperationException()
+ }
+
+ @Throws(IOException::class)
+ override fun observe(path: Path, intervalMillis: Long): PathObservable {
+ throw UnsupportedOperationException()
+ }
- internal fun doRefreshIfNeeded(path: Path) {
- rootProvider.doRefreshIfNeeded(path)
+ @Throws(IOException::class)
+ override fun search(
+ directory: Path,
+ query: String,
+ intervalMillis: Long,
+ listener: (List) -> Unit
+ ) {
+ directory as? ArchivePath ?: throw ProviderMismatchException(directory.toString())
+ WalkFileTreeSearchable.search(directory, query, intervalMillis, listener)
}
}
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..5ffe81da2
--- /dev/null
+++ b/app/src/main/java/me/zhanghai/android/files/provider/archive/ArchivePasswordRequiredException.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 Hai Zhang
+ * All Rights Reserved.
+ */
+
+package me.zhanghai.android.files.provider.archive
+
+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(
+ private val file: Path,
+ reason: String?
+) :
+ UserActionRequiredException(file.toString(), null, reason) {
+
+ 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/LocalArchiveFileAttributeView.kt b/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileAttributeView.kt
deleted file mode 100644
index 534735925..000000000
--- a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileAttributeView.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (c) 2019 Hai Zhang
- * All Rights Reserved.
- */
-
-package me.zhanghai.android.files.provider.archive
-
-import java8.nio.file.Path
-import java8.nio.file.attribute.FileTime
-import me.zhanghai.android.files.provider.common.ByteString
-import me.zhanghai.android.files.provider.common.PosixFileAttributeView
-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 java.io.IOException
-
-internal class LocalArchiveFileAttributeView(private val path: Path) : PosixFileAttributeView {
- override fun name(): String = NAME
-
- @Throws(IOException::class)
- override fun readAttributes(): ArchiveFileAttributes {
- val fileSystem = path.fileSystem as ArchiveFileSystem
- val entry = fileSystem.getEntryAsLocal(path)
- return ArchiveFileAttributes.from(fileSystem.archiveFile, entry)
- }
-
- override fun setTimes(
- lastModifiedTime: FileTime?,
- lastAccessTime: FileTime?,
- createTime: FileTime?
- ) {
- throw UnsupportedOperationException()
- }
-
- override fun setOwner(owner: PosixUser) {
- throw UnsupportedOperationException()
- }
-
- override fun setGroup(group: PosixGroup) {
- throw UnsupportedOperationException()
- }
-
- override fun setMode(mode: Set) {
- throw UnsupportedOperationException()
- }
-
- override fun setSeLinuxContext(context: ByteString) {
- throw UnsupportedOperationException()
- }
-
- override fun restoreSeLinuxContext() {
- throw UnsupportedOperationException()
- }
-
- companion object {
- private val NAME = ArchiveFileSystemProvider.scheme
-
- val SUPPORTED_NAMES = setOf("basic", "posix", NAME)
- }
-}
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
deleted file mode 100644
index 714c1a139..000000000
--- a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileStore.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (c) 2019 Hai Zhang
- * All Rights Reserved.
- */
-
-package me.zhanghai.android.files.provider.archive
-
-import java8.nio.file.Path
-import java8.nio.file.attribute.FileAttributeView
-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() {
- override fun refresh() {}
-
- override fun name(): String = archiveFile.toString()
-
- override fun type(): String = MimeType.guessFromPath(archiveFile.toString()).value
-
- override fun isReadOnly(): Boolean = true
-
- @Throws(IOException::class)
- override fun setReadOnly(readOnly: Boolean) {
- throw UnsupportedOptionsException()
- }
-
- @Throws(IOException::class)
- override fun getTotalSpace(): Long = archiveFile.size()
-
- override fun getUsableSpace(): Long = 0
-
- override fun getUnallocatedSpace(): Long = 0
-
- override fun supportsFileAttributeView(type: Class): Boolean =
- ArchiveFileSystemProvider.supportsFileAttributeView(type)
-
- override fun supportsFileAttributeView(name: String): Boolean =
- name in ArchiveFileAttributeView.SUPPORTED_NAMES
-}
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
deleted file mode 100644
index 0cca8a316..000000000
--- a/app/src/main/java/me/zhanghai/android/files/provider/archive/LocalArchiveFileSystem.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-/*
- * Copyright (c) 2019 Hai Zhang
- * All Rights Reserved.
- */
-
-package me.zhanghai.android.files.provider.archive
-
-import java8.nio.file.ClosedFileSystemException
-import java8.nio.file.FileStore
-import java8.nio.file.FileSystem
-import java8.nio.file.NoSuchFileException
-import java8.nio.file.NotDirectoryException
-import java8.nio.file.Path
-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.archive.archiver.ArchiveReader
-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
-
-internal class LocalArchiveFileSystem(
- private val fileSystem: ArchiveFileSystem,
- private val provider: ArchiveFileSystemProvider,
- val archiveFile: Path
-) : FileSystem(), ByteStringListPathCreator {
- val rootDirectory = ArchivePath(fileSystem, SEPARATOR_BYTE_STRING)
-
- init {
- if (!rootDirectory.isAbsolute) {
- throw AssertionError("Root directory $rootDirectory must be absolute")
- }
- if (rootDirectory.nameCount != 0) {
- throw AssertionError("Root directory $rootDirectory must contain no names")
- }
- }
-
- val defaultDirectory: ArchivePath
- get() = rootDirectory
-
- private val lock = Any()
-
- private var isOpen = true
-
- private var isRefreshNeeded = true
-
- private var entries: Map? = null
-
- private var tree: Map>? = null
-
- @Throws(IOException::class)
- fun getEntry(path: Path): ArchiveEntry =
- synchronized(lock) {
- ensureEntriesLocked()
- getEntryLocked(path)
- }
-
- @Throws(IOException::class)
- private fun getEntryLocked(path: Path): ArchiveEntry =
- synchronized(lock) {
- entries!![path] ?: throw NoSuchFileException(path.toString())
- }
-
- @Throws(IOException::class)
- fun newInputStream(file: Path): InputStream =
- synchronized(lock) {
- ensureEntriesLocked()
- val entry = getEntryLocked(file)
- ArchiveReader.newInputStream(archiveFile, entry)
- }
-
- @Throws(IOException::class)
- fun getDirectoryChildren(directory: Path): List =
- synchronized(lock) {
- ensureEntriesLocked()
- val entry = getEntryLocked(directory)
- if (!entry.isDirectory) {
- throw NotDirectoryException(directory.toString())
- }
- tree!![directory]!!
- }
-
- @Throws(IOException::class)
- fun readSymbolicLink(link: Path): String =
- synchronized(lock) {
- ensureEntriesLocked()
- val entry = getEntryLocked(link)
- ArchiveReader.readSymbolicLink(archiveFile, entry)
- }
-
- fun refresh() {
- synchronized(lock) {
- if (!isOpen) {
- throw ClosedFileSystemException()
- }
- isRefreshNeeded = true
- }
- }
-
- @Throws(IOException::class)
- private fun ensureEntriesLocked() {
- if (!isOpen) {
- throw ClosedFileSystemException()
- }
- if (isRefreshNeeded) {
- val entriesAndTree = ArchiveReader.readEntries(archiveFile, rootDirectory)
- entries = entriesAndTree.first
- tree = entriesAndTree.second
- isRefreshNeeded = false
- }
- }
-
- override fun provider(): FileSystemProvider = provider
-
- override fun close() {
- synchronized(lock) {
- if (!isOpen) {
- return
- }
- provider.removeFileSystem(fileSystem)
- isRefreshNeeded = false
- entries = null
- tree = null
- isOpen = false
- }
- }
-
- override fun isOpen(): Boolean = synchronized(lock) { isOpen }
-
- override fun isReadOnly(): Boolean = true
-
- override fun getSeparator(): String = SEPARATOR_STRING
-
- override fun getRootDirectories(): Iterable