mirror of
https://github.com/zhanghai/MaterialFiles
synced 2024-07-01 06:04:19 +00:00
Feat!: Switch to Compose Multiplatform
This commit is contained in:
parent
c44710e777
commit
2153e6d9d1
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,9 +1,9 @@
|
|||
/.gradle/
|
||||
/.idea/
|
||||
/.kotlin/
|
||||
/build/
|
||||
/captures/
|
||||
/local.properties
|
||||
/signing.jks
|
||||
/signing.properties
|
||||
/*/.gradle/
|
||||
/*/build/
|
||||
.DS_Store
|
||||
*.iml
|
||||
|
|
4
app/.gitignore
vendored
4
app/.gitignore
vendored
|
@ -1,4 +0,0 @@
|
|||
/.cxx/
|
||||
/.externalNativeBuild/
|
||||
/build/
|
||||
/out/
|
|
@ -1,21 +0,0 @@
|
|||
cmake_minimum_required(VERSION 3.6)
|
||||
|
||||
project(MaterialFiles C)
|
||||
|
||||
add_library(hiddenapi SHARED
|
||||
src/main/jni/hiddenapi.c)
|
||||
target_compile_options(hiddenapi
|
||||
PRIVATE
|
||||
-Wall
|
||||
-Werror)
|
||||
|
||||
find_library(LOG_LIBRARY log)
|
||||
add_library(syscall SHARED
|
||||
src/main/jni/syscall.c)
|
||||
target_compile_options(syscall
|
||||
PRIVATE
|
||||
-Wall
|
||||
-Werror)
|
||||
target_link_libraries(syscall
|
||||
PRIVATE
|
||||
${LOG_LIBRARY})
|
218
app/build.gradle
218
app/build.gradle
|
@ -1,218 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
apply from: '../signing.gradle'
|
||||
|
||||
//#ifdef NONFREE
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.4.2'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.1'
|
||||
}
|
||||
}
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
//#endif
|
||||
|
||||
android {
|
||||
namespace 'me.zhanghai.android.files'
|
||||
buildToolsVersion = '34.0.0'
|
||||
compileSdk 34
|
||||
ndkVersion '26.3.11579264'
|
||||
defaultConfig {
|
||||
applicationId 'me.zhanghai.android.files'
|
||||
minSdk 21
|
||||
// Not supporting notification runtime permission yet.
|
||||
//noinspection OldTargetApi
|
||||
targetSdk 34
|
||||
versionCode 37
|
||||
versionName '1.7.2'
|
||||
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 {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
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
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
//#ifdef NONFREE
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled true
|
||||
}
|
||||
//#endif
|
||||
}
|
||||
}
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging true
|
||||
}
|
||||
resources {
|
||||
excludes += [
|
||||
'META-INF/DEPENDENCIES',
|
||||
'org/bouncycastle/pqc/crypto/picnic/*'
|
||||
]
|
||||
}
|
||||
}
|
||||
bundle {
|
||||
language {
|
||||
enableSplit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation('com.github.zhanghai:dav4jvm:c317607') {
|
||||
exclude group: 'org.ogce', module: 'xpp3'
|
||||
}
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.github.topjohnwu.libsu:service:5.2.2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
// kotlinx-coroutines-android depends on kotlin-stdlib-jdk8
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
def kotlinx_coroutines_version = '1.8.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version"
|
||||
|
||||
implementation 'androidx.activity:activity-ktx:1.9.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.core:core-ktx:1.13.1'
|
||||
implementation 'androidx.drawerlayout:drawerlayout:1.2.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.7'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.7.1'
|
||||
def androidx_lifecycle_version = '2.8.1'
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$androidx_lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$androidx_lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$androidx_lifecycle_version"
|
||||
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.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.drakeet.drawer:drawer:1.0.3'
|
||||
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'
|
||||
}
|
||||
// SSHJ 0.36.0 requires Java 8.
|
||||
//noinspection GradleDependency
|
||||
implementation ('com.hierynomus:sshj:0.35.0') {
|
||||
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
|
||||
}
|
||||
implementation 'com.jakewharton.threetenabp:threetenabp:1.4.6'
|
||||
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 'com.google.guava:guava:33.0.0-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'
|
||||
// Commons Net 3.9.0 started using java.time.Duration in FTPClient.
|
||||
//noinspection GradleDependency
|
||||
implementation 'commons-net:commons-net:3.8.0'
|
||||
// LicensesDialog 2.2.0 pulls in androidx.webkit and uses setForceDark() instead of correctly
|
||||
// setting colors.
|
||||
//noinspection GradleDependency
|
||||
implementation 'de.psdev.licensesdialog:licensesdialog:2.1.0'
|
||||
// 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:13.1.5'
|
||||
implementation ('eu.agno3.jcifs:jcifs-ng:2.1.10') {
|
||||
exclude group: 'org.bouncycastle', module: 'bcprov-jdk18on'
|
||||
}
|
||||
implementation 'org.bouncycastle:bcprov-jdk15to18:1.77'
|
||||
implementation platform('io.coil-kt:coil-bom:2.6.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.0.3'
|
||||
implementation 'me.zhanghai.android.libselinux:library:2.1.0'
|
||||
implementation 'me.zhanghai.android.retrofile:library:1.1.1'
|
||||
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
|
||||
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.4'
|
||||
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
|
||||
// (DIRMINA-1123) and NetworkChannel.supportedOptions() (DIRMINA-1130).
|
||||
implementation ('org.apache.mina:mina-core') {
|
||||
version {
|
||||
strictly '2.1.3'
|
||||
}
|
||||
}
|
||||
// Also a dependency of jCIFS-NG.
|
||||
implementation 'org.slf4j:slf4j-android:1.7.36'
|
||||
|
||||
//#ifdef NONFREE
|
||||
implementation platform('com.google.firebase:firebase-bom:33.1.0')
|
||||
implementation 'com.google.firebase:firebase-analytics'
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
//#endif
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "728501351509",
|
||||
"firebase_url": "https://zhanghai-materialfiles.firebaseio.com",
|
||||
"project_id": "zhanghai-materialfiles",
|
||||
"storage_bucket": "zhanghai-materialfiles.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:728501351509:android:007cb5eb181702a9",
|
||||
"android_client_info": {
|
||||
"package_name": "me.zhanghai.android.files"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "728501351509-3ul5c5u8jpd9q9gi4e7537bd31r5nkr5.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyDUcQAOKRcCjsA-e5-cZHbt4HQQFDP4wA4"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": [
|
||||
{
|
||||
"client_id": "728501351509-3ul5c5u8jpd9q9gi4e7537bd31r5nkr5.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
57
app/proguard-rules.pro
vendored
57
app/proguard-rules.pro
vendored
|
@ -1,57 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
# Native methods
|
||||
# https://www.guardsquare.com/en/products/proguard/manual/examples#native
|
||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||
native <methods>;
|
||||
}
|
||||
|
||||
# App
|
||||
-keep class me.zhanghai.android.files.** implements androidx.appcompat.view.CollapsibleActionView { *; }
|
||||
-keep class me.zhanghai.android.files.provider.common.ByteString { *; }
|
||||
-keep class me.zhanghai.android.files.provider.linux.syscall.** { *; }
|
||||
-keepnames class * extends java.lang.Exception
|
||||
# For Class.getEnumConstants()
|
||||
-keepclassmembers enum * {
|
||||
public static **[] values();
|
||||
}
|
||||
-keepnames class me.zhanghai.android.files.** implements android.os.Parcelable
|
||||
|
||||
# Apache FtpServer
|
||||
-keepclassmembers class * implements org.apache.mina.core.service.IoProcessor {
|
||||
public <init>(java.util.concurrent.ExecutorService);
|
||||
public <init>(java.util.concurrent.Executor);
|
||||
public <init>();
|
||||
}
|
||||
|
||||
# Bouncy Castle
|
||||
-keep class org.bouncycastle.jcajce.provider.** { *; }
|
||||
-keep class org.bouncycastle.jce.provider.** { *; }
|
||||
|
||||
# SMBJ
|
||||
-dontwarn javax.el.**
|
||||
-dontwarn org.ietf.jgss.**
|
||||
-dontwarn sun.security.x509.X509Key
|
||||
|
||||
# SMBJ-RPC
|
||||
-dontwarn java.rmi.UnmarshalException
|
|
@ -1,399 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
~ Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
~ All Rights Reserved.
|
||||
-->
|
||||
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<!-- Shizuku requires API 23. -->
|
||||
<uses-sdk tools:overrideLibrary="rikka.shizuku.aidl,rikka.shizuku.api,rikka.shizuku.shared" />
|
||||
|
||||
<!--
|
||||
~ Samsung DeX requires explicitly setting android:resizeableActivity="true" for the app to be
|
||||
~ resizable.
|
||||
~ TODO: Remove this attribute once Samsung respects the default value for it.
|
||||
-->
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:banner="@drawable/banner"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="true"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/netework_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:requestRawExternalStorageAccess="true"
|
||||
android:resizeableActivity="true"
|
||||
android:roundIcon="@mipmap/launcher_icon"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.MaterialFiles"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.filelist.FileListActivity"
|
||||
android:exported="true"
|
||||
android:visibleToInstantApps="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="inode/directory" />
|
||||
<data android:mimeType="resource/folder" />
|
||||
<data android:mimeType="vnd.android.document/directory" />
|
||||
</intent-filter>
|
||||
<!-- @see me.zhanghai.android.files.file.isSupportedArchive -->
|
||||
<intent-filter
|
||||
android:label="@string/archive_viewer_title"
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/gzip" />
|
||||
<data android:mimeType="application/java-archive" />
|
||||
<data android:mimeType="application/rar" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="application/zstd" />
|
||||
<data android:mimeType="application/vnd.android.package-archive" />
|
||||
<data android:mimeType="application/vnd.debian.binary-package" />
|
||||
<data android:mimeType="application/vnd.ms-cab-compressed" />
|
||||
<data android:mimeType="application/vnd.rar" />
|
||||
<data android:mimeType="application/x-bzip2" />
|
||||
<data android:mimeType="application/x-compress" />
|
||||
<data android:mimeType="application/x-cpio" />
|
||||
<data android:mimeType="application/x-deb" />
|
||||
<data android:mimeType="application/x-debian-package" />
|
||||
<data android:mimeType="application/x-gtar" />
|
||||
<data android:mimeType="application/x-gtar-compressed" />
|
||||
<data android:mimeType="application/x-java-archive" />
|
||||
<data android:mimeType="application/x-lzma" />
|
||||
<data android:mimeType="application/x-tar" />
|
||||
<data android:mimeType="application/x-xz" />
|
||||
<data android:mimeType="application/x-7z-compressed" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<!-- @see https://android.googlesource.com/platform/packages/apps/DocumentsUI/+/main/AndroidManifest.xml -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.OPEN_DOCUMENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_DOCUMENT" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<!--
|
||||
~ Unusable until we implement DocumentsProvider.
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.OPEN_DOCUMENT_TREE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="me.zhanghai.android.files.intent.action.VIEW_DOWNLOADS" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<!--
|
||||
~ Using android:documentLaunchMode="always" gives a better result than
|
||||
~ Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK. Not sure why though.
|
||||
-->
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.filelist.OpenFileActivity"
|
||||
android:documentLaunchMode="always"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent">
|
||||
<intent-filter>
|
||||
<action android:name="me.zhanghai.android.files.intent.action.OPEN_FILE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.filelist.EditFileActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:icon="@drawable/edit_icon"
|
||||
android:label="@string/file_edit_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.filelist.OpenFileAsDialogActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:icon="@drawable/open_as_icon"
|
||||
android:label="@string/file_open_as_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.AddStorageDialogActivity"
|
||||
android:label="@string/storage_add_storage_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.EditDeviceStorageDialogActivity"
|
||||
android:label="@string/storage_edit_device_storage_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.AddExternalStorageShortcutActivity"
|
||||
android:label="@string/storage_add_external_storage_shortcut_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.EditExternalStorageShortcutDialogActivity"
|
||||
android:label="@string/storage_edit_external_storage_shortcut_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.AddDocumentTreeActivity"
|
||||
android:label="@string/storage_add_document_tree_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.EditDocumentTreeDialogActivity"
|
||||
android:label="@string/storage_edit_document_tree_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.EditFtpServerActivity"
|
||||
android:label="@string/storage_edit_ftp_server_title_edit"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.EditSftpServerActivity"
|
||||
android:label="@string/storage_edit_sftp_server_title_edit"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.AddLanSmbServerActivity"
|
||||
android:label="@string/storage_add_lan_smb_server_title"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.EditSmbServerActivity"
|
||||
android:label="@string/storage_edit_smb_server_title_edit"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.EditWebDavServerActivity"
|
||||
android:label="@string/storage_edit_webdav_server_title_edit"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.navigation.EditBookmarkDirectoryDialogActivity"
|
||||
android:label="@string/navigation_edit_bookmark_directory_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.ftpserver.FtpServerActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/ftp_server_title"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.MaterialFiles">
|
||||
<intent-filter>
|
||||
<action android:name="me.zhanghai.android.files.intent.action.MANAGE_FTP_SERVER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.settings.SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/settings_title"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.MaterialFiles">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.storage.StorageListActivity"
|
||||
android:label="@string/storage_list_title"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.settings.StandardDirectoryListActivity"
|
||||
android:label="@string/settings_standard_directory_list_title"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.settings.BookmarkDirectoryListActivity"
|
||||
android:label="@string/settings_bookmark_directory_list_title"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.about.AboutActivity"
|
||||
android:label="@string/about_title"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.MaterialFiles" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.fileaction.ArchivePasswordDialogActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.filejob.FileJobErrorDialogActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.filejob.FileJobConflictDialogActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent" />
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.viewer.saveas.SaveAsActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:exported="true"
|
||||
android:label="@string/save_as_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Translucent">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:mimeType="*/*"
|
||||
android:scheme="content" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.viewer.text.TextEditorActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/text_editor_title"
|
||||
android:theme="@style/Theme.MaterialFiles">
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/ecmascript" />
|
||||
<data android:mimeType="application/javascript" />
|
||||
<data android:mimeType="application/json" />
|
||||
<data android:mimeType="application/typescript" />
|
||||
<data android:mimeType="application/x-sh" />
|
||||
<data android:mimeType="application/x-shellscript" />
|
||||
<data android:mimeType="application/xml" />
|
||||
<data android:mimeType="application/yaml" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="me.zhanghai.android.files.viewer.image.ImageViewerActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/image_viewer_title"
|
||||
android:theme="@style/Theme.MaterialFiles.Immersive">
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="me.zhanghai.android.files.filejob.FileJobService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service android:name="me.zhanghai.android.files.ftpserver.FtpServerService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<service
|
||||
android:name="me.zhanghai.android.files.ftpserver.FtpServerTileService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/shared_directory_icon_white_24dp"
|
||||
android:label="@string/ftp_server_title"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="me.zhanghai.android.files.app.AppProvider"
|
||||
android:authorities="@string/app_provider_authority"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="me.zhanghai.android.files.file.FileProvider"
|
||||
android:authorities="@string/file_provider_authority"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<receiver android:name="me.zhanghai.android.files.filejob.FileJobReceiver" />
|
||||
|
||||
<receiver android:name="me.zhanghai.android.files.ftpserver.FtpServerReceiver" />
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="false" />
|
||||
</application>
|
||||
</manifest>
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.common;
|
||||
|
||||
parcelable ParcelableFileTime;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.common;
|
||||
|
||||
parcelable ParcelablePosixFileMode;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.common;
|
||||
|
||||
parcelable PosixGroup;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.common;
|
||||
|
||||
parcelable PosixUser;
|
|
@ -1,23 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
import me.zhanghai.android.files.provider.remote.IRemoteFileSystem;
|
||||
import me.zhanghai.android.files.provider.remote.IRemoteFileSystemProvider;
|
||||
import me.zhanghai.android.files.provider.remote.IRemotePosixFileAttributeView;
|
||||
import me.zhanghai.android.files.provider.remote.IRemotePosixFileStore;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableObject;
|
||||
|
||||
interface IRemoteFileService {
|
||||
IRemoteFileSystemProvider getRemoteFileSystemProviderInterface(String scheme);
|
||||
|
||||
IRemoteFileSystem getRemoteFileSystemInterface(in ParcelableObject fileSystem);
|
||||
|
||||
IRemotePosixFileStore getRemotePosixFileStoreInterface(in ParcelableObject fileStore);
|
||||
|
||||
IRemotePosixFileAttributeView getRemotePosixFileAttributeViewInterface(
|
||||
in ParcelableObject attributeView
|
||||
);
|
||||
|
||||
void setArchivePasswords(in ParcelableObject fileSystem, in List<String> passwords);
|
||||
|
||||
void refreshArchiveFileSystem(in ParcelableObject fileSystem);
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableException;
|
||||
|
||||
interface IRemoteFileSystem {
|
||||
void close(out ParcelableException exception);
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableCopyOptions;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableDirectoryStream;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableException;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableFileAttributes;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableObject;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelablePathListConsumer;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableSerializable;
|
||||
import me.zhanghai.android.files.provider.remote.RemotePathObservable;
|
||||
import me.zhanghai.android.files.provider.remote.RemoteInputStream;
|
||||
import me.zhanghai.android.files.provider.remote.RemoteSeekableByteChannel;
|
||||
import me.zhanghai.android.files.util.RemoteCallback;
|
||||
|
||||
interface IRemoteFileSystemProvider {
|
||||
RemoteInputStream newInputStream(
|
||||
in ParcelableObject file,
|
||||
in ParcelableSerializable options,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
RemoteSeekableByteChannel newByteChannel(
|
||||
in ParcelableObject file,
|
||||
in ParcelableSerializable options,
|
||||
in ParcelableFileAttributes attributes,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
ParcelableDirectoryStream newDirectoryStream(
|
||||
in ParcelableObject directory,
|
||||
in ParcelableObject filter,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
void createDirectory(
|
||||
in ParcelableObject directory,
|
||||
in ParcelableFileAttributes attributes,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
void createSymbolicLink(
|
||||
in ParcelableObject link,
|
||||
in ParcelableObject target,
|
||||
in ParcelableFileAttributes attributes,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
void createLink(
|
||||
in ParcelableObject link,
|
||||
in ParcelableObject existing,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
void delete(in ParcelableObject path, out ParcelableException exception);
|
||||
|
||||
ParcelableObject readSymbolicLink(in ParcelableObject link, out ParcelableException exception);
|
||||
|
||||
RemoteCallback copy(
|
||||
in ParcelableObject source,
|
||||
in ParcelableObject target,
|
||||
in ParcelableCopyOptions options,
|
||||
in RemoteCallback callback
|
||||
);
|
||||
|
||||
RemoteCallback move(
|
||||
in ParcelableObject source,
|
||||
in ParcelableObject target,
|
||||
in ParcelableCopyOptions options,
|
||||
in RemoteCallback callback
|
||||
);
|
||||
|
||||
boolean isSameFile(
|
||||
in ParcelableObject path,
|
||||
in ParcelableObject path2,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
boolean isHidden(in ParcelableObject path, out ParcelableException exception);
|
||||
|
||||
ParcelableObject getFileStore(in ParcelableObject path, out ParcelableException exception);
|
||||
|
||||
void checkAccess(
|
||||
in ParcelableObject path,
|
||||
in ParcelableSerializable modes,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
ParcelableObject readAttributes(
|
||||
in ParcelableObject path,
|
||||
in ParcelableSerializable type,
|
||||
in ParcelableSerializable options,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
RemotePathObservable observe(
|
||||
in ParcelableObject path,
|
||||
long intervalMillis,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
RemoteCallback search(
|
||||
in ParcelableObject directory,
|
||||
in String query,
|
||||
long intervalMillis,
|
||||
in ParcelablePathListConsumer listener,
|
||||
in RemoteCallback callback
|
||||
);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableException;
|
||||
|
||||
interface IRemoteInputStream {
|
||||
int read(out ParcelableException exception);
|
||||
|
||||
int read2(out byte[] buffer, out ParcelableException exception);
|
||||
|
||||
long skip(long size, out ParcelableException exception);
|
||||
|
||||
int available(out ParcelableException exception);
|
||||
|
||||
void close(out ParcelableException exception);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableException;
|
||||
import me.zhanghai.android.files.util.RemoteCallback;
|
||||
|
||||
interface IRemotePathObservable {
|
||||
void addObserver(in RemoteCallback observer);
|
||||
|
||||
void close(out ParcelableException exception);
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
import me.zhanghai.android.files.provider.common.ParcelableFileTime;
|
||||
import me.zhanghai.android.files.provider.common.ParcelablePosixFileMode;
|
||||
import me.zhanghai.android.files.provider.common.PosixGroup;
|
||||
import me.zhanghai.android.files.provider.common.PosixUser;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableException;
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableObject;
|
||||
|
||||
interface IRemotePosixFileAttributeView {
|
||||
ParcelableObject readAttributes(out ParcelableException exception);
|
||||
|
||||
void setTimes(
|
||||
in ParcelableFileTime lastModifiedTime,
|
||||
in ParcelableFileTime lastAccessTime,
|
||||
in ParcelableFileTime createTime,
|
||||
out ParcelableException exception
|
||||
);
|
||||
|
||||
void setOwner(in PosixUser owner, out ParcelableException exception);
|
||||
|
||||
void setGroup(in PosixGroup group, out ParcelableException exception);
|
||||
|
||||
void setMode(in ParcelablePosixFileMode mode, out ParcelableException exception);
|
||||
|
||||
void setSeLinuxContext(in ParcelableObject context, out ParcelableException exception);
|
||||
|
||||
void restoreSeLinuxContext(out ParcelableException exception);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableException;
|
||||
|
||||
interface IRemotePosixFileStore {
|
||||
void setReadOnly(boolean readOnly, out ParcelableException exception);
|
||||
|
||||
long getTotalSpace(out ParcelableException exception);
|
||||
|
||||
long getUsableSpace(out ParcelableException exception);
|
||||
|
||||
long getUnallocatedSpace(out ParcelableException exception);
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
import me.zhanghai.android.files.provider.remote.ParcelableException;
|
||||
|
||||
interface IRemoteSeekableByteChannel {
|
||||
int read(out byte[] destination, out ParcelableException exception);
|
||||
|
||||
int write(in byte[] source, out ParcelableException exception);
|
||||
|
||||
long position(out ParcelableException exception);
|
||||
|
||||
void position2(long newPosition, out ParcelableException exception);
|
||||
|
||||
long size(out ParcelableException exception);
|
||||
|
||||
void truncate(long size, out ParcelableException exception);
|
||||
|
||||
void force(boolean metaData, out ParcelableException exception);
|
||||
|
||||
void close(out ParcelableException exception);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable ParcelableCopyOptions;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable ParcelableDirectoryStream;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable ParcelableException;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable ParcelableFileAttributes;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable ParcelableObject;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable ParcelablePathListConsumer;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable ParcelableSerializable;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable RemoteInputStream;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable RemotePathObservable;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.provider.remote;
|
||||
|
||||
parcelable RemoteSeekableByteChannel;
|
|
@ -1,7 +0,0 @@
|
|||
package me.zhanghai.android.files.util;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
interface IRemoteCallback {
|
||||
void sendResult(in Bundle result);
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.util;
|
||||
|
||||
parcelable ParcelSlicedList;
|
|
@ -1,3 +0,0 @@
|
|||
package me.zhanghai.android.files.util;
|
||||
|
||||
parcelable RemoteCallback;
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package androidx.appcompat.app;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class AppCompatDelegateCompat {
|
||||
private AppCompatDelegateCompat() {}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
public static int mapNightMode(@NonNull AppCompatDelegate delegate, @NonNull Context context,
|
||||
int mode) {
|
||||
return ((AppCompatDelegateImpl) delegate).mapNightMode(context, mode);
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package androidx.appcompat.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
|
||||
public class FixPaddingListPopupWindow extends ListPopupWindow {
|
||||
public FixPaddingListPopupWindow(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public FixPaddingListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public FixPaddingListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
@AttrRes int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public FixPaddingListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
@AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
DropDownListView createDropDownListView(@NonNull Context context, boolean hijackFocus) {
|
||||
return new FixPaddingDropDownListView(context, hijackFocus);
|
||||
}
|
||||
|
||||
private static class FixPaddingDropDownListView extends DropDownListView {
|
||||
public FixPaddingDropDownListView(Context context, boolean hijackFocus) {
|
||||
super(context, hijackFocus);
|
||||
}
|
||||
|
||||
// DropDownListView.measureHeightOfChildrenCompat() uses list padding instead of regular
|
||||
// padding, which isn't initialized before onMeasure() so returns no padding for the first
|
||||
// time. And ListPopupWindow.buildDropDown() adds the regular padding back every time, which
|
||||
// will double the padding after the first show.
|
||||
@Override
|
||||
public int measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition,
|
||||
int endPosition, int maxHeight,
|
||||
int disallowPartialChildPosition) {
|
||||
int height = super.measureHeightOfChildrenCompat(widthMeasureSpec, startPosition,
|
||||
endPosition, maxHeight, disallowPartialChildPosition);
|
||||
height -= getListPaddingTop() + getListPaddingBottom();
|
||||
return height;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package androidx.swiperefreshlayout.widget;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.ColorUtils;
|
||||
import me.zhanghai.android.files.R;
|
||||
import me.zhanghai.android.files.compat.ContextCompatKt;
|
||||
import me.zhanghai.android.files.util.ContextExtensionsKt;
|
||||
|
||||
public class ThemedSwipeRefreshLayout extends SwipeRefreshLayout {
|
||||
public ThemedSwipeRefreshLayout(@NonNull Context context) {
|
||||
super(context);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public ThemedSwipeRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
Context context = getContext();
|
||||
boolean isMaterial3Theme = ContextExtensionsKt.isMaterial3Theme(context);
|
||||
int backgroundColor;
|
||||
if (isMaterial3Theme) {
|
||||
int surfaceColor = ContextExtensionsKt.getColorByAttr(context,
|
||||
com.google.android.material.R.attr.colorSurface);
|
||||
@SuppressLint("PrivateResource")
|
||||
int overlayColor = ContextCompatKt.getColorCompat(context,
|
||||
com.google.android.material.R.color.m3_popupmenu_overlay_color);
|
||||
backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor);
|
||||
} else {
|
||||
backgroundColor = ContextExtensionsKt.getColorByAttr(context,
|
||||
androidx.appcompat.R.attr.colorBackgroundFloating);
|
||||
}
|
||||
((ShapeDrawable) mCircleView.getBackground()).getPaint().setColor(backgroundColor);
|
||||
setColorSchemeColors(ContextExtensionsKt.getColorByAttr(context,
|
||||
androidx.appcompat.R.attr.colorAccent));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
View child = getChildView();
|
||||
if (child != null) {
|
||||
measureChild(child, widthMeasureSpec, heightMeasureSpec);
|
||||
setMeasuredDimension(child.getMeasuredWidth() + getPaddingLeft() + getPaddingRight(),
|
||||
child.getMeasuredHeight() + getPaddingTop() + getPaddingBottom());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private View getChildView() {
|
||||
for (int i = 0; i < getChildCount(); ++i) {
|
||||
View child = getChildAt(i);
|
||||
if (!child.equals(mCircleView)) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* 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<Response> sendRequest) throws DavException, IOException {
|
||||
return davResource.followRedirects$build(sendRequest);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package com.google.android.material.appbar;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
public class OnWindowInsetChangedAppBarLayout extends AppBarLayout {
|
||||
public OnWindowInsetChangedAppBarLayout(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public OnWindowInsetChangedAppBarLayout(@NonNull Context context,
|
||||
@Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public OnWindowInsetChangedAppBarLayout(@NonNull Context context, @Nullable AttributeSet attrs,
|
||||
@AttrRes int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsetsCompat onWindowInsetChanged(@NonNull WindowInsetsCompat insets) {
|
||||
return super.onWindowInsetChanged(insets);
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package com.google.android.material.shape;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.elevation.ElevationOverlayProvider;
|
||||
|
||||
public class MaterialShapeDrawableAccessor {
|
||||
private MaterialShapeDrawableAccessor() {}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
public static ElevationOverlayProvider getElevationOverlayProvider(
|
||||
@NonNull MaterialShapeDrawable drawable) {
|
||||
MaterialShapeDrawable.MaterialShapeDrawableState drawableState =
|
||||
(MaterialShapeDrawable.MaterialShapeDrawableState) drawable.getConstantState();
|
||||
return drawableState.elevationOverlayProvider;
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
public static void setElevationOverlayProvider(
|
||||
@NonNull MaterialShapeDrawable drawable,
|
||||
@Nullable ElevationOverlayProvider elevationOverlayProvider) {
|
||||
MaterialShapeDrawable.MaterialShapeDrawableState drawableState =
|
||||
(MaterialShapeDrawable.MaterialShapeDrawableState) drawable.getConstantState();
|
||||
drawableState.elevationOverlayProvider = elevationOverlayProvider;
|
||||
}
|
||||
|
||||
public static void updateZ(@NonNull MaterialShapeDrawable drawable) {
|
||||
final float parentAbsoluteElevation = drawable.getParentAbsoluteElevation();
|
||||
drawable.setParentAbsoluteElevation(parentAbsoluteElevation + 1);
|
||||
drawable.setParentAbsoluteElevation(parentAbsoluteElevation);
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package com.hierynomus.smbj.share;
|
||||
|
||||
import com.hierynomus.mssmb2.messages.SMB2ReadResponse;
|
||||
import com.hierynomus.smbj.common.SMBRuntimeException;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class FileAccessor {
|
||||
private FileAccessor() {}
|
||||
|
||||
/**
|
||||
* @see File#readAsync(long, int)
|
||||
*/
|
||||
@NonNull
|
||||
public static Future<SMB2ReadResponse> readAsync(@NonNull File file, long offset, int length)
|
||||
throws SMBRuntimeException {
|
||||
return file.readAsync(offset, length);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package com.hierynomus.smbj.share;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.hierynomus.mssmb2.SMB2FileId;
|
||||
import com.hierynomus.mssmb2.messages.SMB2IoctlResponse;
|
||||
import com.hierynomus.smbj.io.ArrayByteChunkProvider;
|
||||
import com.hierynomus.smbj.io.ByteChunkProvider;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class ShareAccessor {
|
||||
private ShareAccessor() {}
|
||||
|
||||
/**
|
||||
* This ioctl() variant allows passing in the {@param statusHandler}.
|
||||
*
|
||||
* @see Share#ioctl(com.hierynomus.mssmb2.SMB2FileId, long, boolean, ByteChunkProvider, int)
|
||||
*/
|
||||
@NonNull
|
||||
public static SMB2IoctlResponse ioctl(@NonNull Share share, @NonNull SMB2FileId fileId,
|
||||
long ctlCode, boolean isFsCtl, @NonNull byte[] inData,
|
||||
int inOffset, int inLength,
|
||||
@NonNull StatusHandler statusHandler, long timeout) {
|
||||
final ByteChunkProvider inputData = new ArrayByteChunkProvider(inData, inOffset, inLength,
|
||||
0);
|
||||
final Future<SMB2IoctlResponse> future = share.ioctlAsync(fileId, ctlCode, isFsCtl,
|
||||
inputData, -1);
|
||||
return share.receive(future, "IOCTL", fileId, statusHandler, timeout);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.about
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.add
|
||||
import androidx.fragment.app.commit
|
||||
import me.zhanghai.android.files.app.AppActivity
|
||||
|
||||
class AboutActivity : AppActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Calls ensureSubDecor().
|
||||
findViewById<View>(android.R.id.content)
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.commit { add<AboutFragment>(android.R.id.content) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.about
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import me.zhanghai.android.files.databinding.AboutFragmentBinding
|
||||
import me.zhanghai.android.files.ui.LicensesDialogFragment
|
||||
import me.zhanghai.android.files.util.createViewIntent
|
||||
import me.zhanghai.android.files.util.startActivitySafe
|
||||
|
||||
class AboutFragment : Fragment() {
|
||||
private lateinit var binding: AboutFragmentBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
AboutFragmentBinding.inflate(inflater, container, false)
|
||||
.also { binding = it }
|
||||
.root
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
val activity = requireActivity() as AppCompatActivity
|
||||
activity.setSupportActionBar(binding.toolbar)
|
||||
activity.supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
binding.gitHubLayout.setOnClickListener { startActivitySafe(GITHUB_URI.createViewIntent()) }
|
||||
binding.licensesLayout.setOnClickListener { LicensesDialogFragment.show(this) }
|
||||
//#ifdef NONFREE
|
||||
binding.privacyPolicyLayout.isVisible = true
|
||||
binding.privacyPolicyLayout.setOnClickListener {
|
||||
startActivitySafe(PRIVACY_POLICY_URI.createViewIntent())
|
||||
}
|
||||
//#endif
|
||||
binding.authorNameLayout.setOnClickListener {
|
||||
startActivitySafe(AUTHOR_RESUME_URI.createViewIntent())
|
||||
}
|
||||
binding.authorGitHubLayout.setOnClickListener {
|
||||
startActivitySafe(AUTHOR_GITHUB_URI.createViewIntent())
|
||||
}
|
||||
binding.authorTwitterLayout.setOnClickListener {
|
||||
startActivitySafe(AUTHOR_TWITTER_URI.createViewIntent())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val GITHUB_URI = Uri.parse("https://github.com/zhanghai/MaterialFiles")
|
||||
private val PRIVACY_POLICY_URI =
|
||||
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_TWITTER_URI = Uri.parse("https://twitter.com/zhanghai95")
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.app
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import me.zhanghai.android.files.theme.custom.CustomThemeHelper
|
||||
import me.zhanghai.android.files.theme.night.NightModeHelper
|
||||
|
||||
abstract class AppActivity : AppCompatActivity() {
|
||||
private var isDelegateCreated = false
|
||||
|
||||
override fun getDelegate(): AppCompatDelegate {
|
||||
val delegate = super.getDelegate()
|
||||
|
||||
if (!isDelegateCreated) {
|
||||
isDelegateCreated = true
|
||||
NightModeHelper.apply(this)
|
||||
}
|
||||
return delegate
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
CustomThemeHelper.apply(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
if (!super.onSupportNavigateUp()) {
|
||||
finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.app
|
||||
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import jcifs.context.SingletonContext
|
||||
import me.zhanghai.android.files.BuildConfig
|
||||
import me.zhanghai.android.files.coil.initializeCoil
|
||||
import me.zhanghai.android.files.filejob.fileJobNotificationTemplate
|
||||
import me.zhanghai.android.files.ftpserver.ftpServerServiceNotificationTemplate
|
||||
import me.zhanghai.android.files.hiddenapi.HiddenApi
|
||||
import me.zhanghai.android.files.provider.FileSystemProviders
|
||||
import me.zhanghai.android.files.settings.Settings
|
||||
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, ::initializeCoil, ::initializeFileSystemProviders, ::upgradeApp,
|
||||
::initializeLiveDataObjects, ::initializeCustomTheme, ::initializeNightMode,
|
||||
::createNotificationChannels
|
||||
)
|
||||
|
||||
private fun initializeCrashlytics() {
|
||||
//#ifdef NONFREE
|
||||
me.zhanghai.android.files.nonfree.CrashlyticsInitializer.initialize()
|
||||
//#endif
|
||||
}
|
||||
|
||||
private fun disableHiddenApiChecks() {
|
||||
HiddenApi.disableHiddenApiChecks()
|
||||
}
|
||||
|
||||
private fun initializeThreeTen() {
|
||||
AndroidThreeTen.init(application)
|
||||
}
|
||||
|
||||
private fun initializeWebViewDebugging() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
WebView.setWebContentsDebuggingEnabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeFileSystemProviders() {
|
||||
FileSystemProviders.install()
|
||||
FileSystemProviders.overflowWatchEvents = true
|
||||
// SingletonContext.init() calls NameServiceClientImpl.initCache() which connects to network.
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute {
|
||||
SingletonContext.init(
|
||||
Properties().apply {
|
||||
setProperty("jcifs.netbios.cachePolicy", "0")
|
||||
setProperty("jcifs.smb.client.maxVersion", "SMB1")
|
||||
}
|
||||
)
|
||||
}
|
||||
FtpClient.authenticator = FtpServerAuthenticator
|
||||
SftpClient.authenticator = SftpServerAuthenticator
|
||||
SmbClient.authenticator = SmbServerAuthenticator
|
||||
WebDavClient.authenticator = WebDavServerAuthenticator
|
||||
}
|
||||
|
||||
private fun initializeLiveDataObjects() {
|
||||
// Force initialization of LiveData objects so that it won't happen on a background thread.
|
||||
StorageVolumeListLiveData.value
|
||||
Settings.FILE_LIST_DEFAULT_DIRECTORY.value
|
||||
}
|
||||
|
||||
private fun initializeCustomTheme() {
|
||||
CustomThemeHelper.initialize(application)
|
||||
}
|
||||
|
||||
private fun initializeNightMode() {
|
||||
NightModeHelper.initialize(application)
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.createNotificationChannels(
|
||||
listOf(
|
||||
backgroundActivityStartNotificationTemplate.channelTemplate,
|
||||
fileJobNotificationTemplate.channelTemplate,
|
||||
ftpServerServiceNotificationTemplate.channelTemplate
|
||||
).map { it.create(application) }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
|
||||
lateinit var application: Application private set
|
||||
|
||||
class AppProvider : ContentProvider() {
|
||||
override fun onCreate(): Boolean {
|
||||
application = context as Application
|
||||
appInitializers.forEach { it() }
|
||||
return true
|
||||
}
|
||||
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<String?>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String?>?,
|
||||
sortOrder: String?
|
||||
): Cursor? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String?>?): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun update(
|
||||
uri: Uri,
|
||||
values: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String?>?
|
||||
): Int {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.app
|
||||
|
||||
import androidx.core.content.edit
|
||||
import me.zhanghai.android.files.BuildConfig
|
||||
|
||||
private const val KEY_VERSION_CODE = "key_version_code"
|
||||
|
||||
private const val VERSION_CODE_BELOW_1_1_0 = 17
|
||||
private const val VERSION_CODE_1_1_0 = 18
|
||||
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
|
||||
get() {
|
||||
if (defaultSharedPreferences.all.isEmpty()) {
|
||||
// This is a new install.
|
||||
lastVersionCode = VERSION_CODE_LATEST
|
||||
return VERSION_CODE_LATEST
|
||||
}
|
||||
return defaultSharedPreferences.getInt(KEY_VERSION_CODE, VERSION_CODE_BELOW_1_1_0)
|
||||
}
|
||||
set(value) {
|
||||
defaultSharedPreferences.edit { putInt(KEY_VERSION_CODE, value) }
|
||||
}
|
||||
|
||||
fun upgradeApp() {
|
||||
upgradeAppFrom(lastVersionCode)
|
||||
lastVersionCode = VERSION_CODE_LATEST
|
||||
}
|
||||
|
||||
private fun upgradeAppFrom(lastVersionCode: Int) {
|
||||
if (lastVersionCode < VERSION_CODE_1_1_0) {
|
||||
upgradeAppTo1_1_0()
|
||||
}
|
||||
if (lastVersionCode < VERSION_CODE_1_2_0) {
|
||||
upgradeAppTo1_2_0()
|
||||
}
|
||||
if (lastVersionCode < VERSION_CODE_1_3_0) {
|
||||
upgradeAppTo1_3_0()
|
||||
}
|
||||
if (lastVersionCode < VERSION_CODE_1_4_0) {
|
||||
upgradeAppTo1_4_0()
|
||||
}
|
||||
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`.
|
||||
}
|
|
@ -1,632 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
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
|
||||
import androidx.core.content.edit
|
||||
import me.zhanghai.android.files.R
|
||||
import me.zhanghai.android.files.compat.PreferenceManagerCompat
|
||||
import me.zhanghai.android.files.compat.getDescriptionCompat
|
||||
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
|
||||
import me.zhanghai.android.files.navigation.BookmarkDirectory
|
||||
import me.zhanghai.android.files.navigation.StandardDirectorySettings
|
||||
import me.zhanghai.android.files.provider.archive.ArchiveFileSystem
|
||||
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
|
||||
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
|
||||
import me.zhanghai.android.files.util.toBase64
|
||||
import me.zhanghai.android.files.util.toByteArray
|
||||
import me.zhanghai.android.files.util.use
|
||||
|
||||
internal fun upgradeAppTo1_1_0() {
|
||||
// Migrate settings.
|
||||
migratePathSetting1_1_0(R.string.pref_key_file_list_default_directory)
|
||||
migrateFileSortOptionsSetting1_1_0()
|
||||
migrateCreateArchiveTypeSetting1_1_0()
|
||||
migrateStandardDirectorySettingsSetting1_1_0()
|
||||
migrateBookmarkDirectoriesSetting1_1_0()
|
||||
migratePathSetting1_1_0(R.string.pref_key_ftp_server_home_directory)
|
||||
for (key in pathSharedPreferences.all.keys) {
|
||||
migrateFileSortOptionsSetting1_1_0(pathSharedPreferences, key)
|
||||
}
|
||||
}
|
||||
|
||||
private const val PARCEL_VAL_PARCELABLE = 4
|
||||
private const val PARCEL_VAL_LIST = 11
|
||||
|
||||
private fun migratePathSetting1_1_0(@StringRes keyRes: Int) {
|
||||
val key = application.getString(keyRes)
|
||||
val oldBytes = defaultSharedPreferences.getString(key, null)?.asBase64()?.toByteArray()
|
||||
?: return
|
||||
val newBytes = try {
|
||||
Parcel.obtain().use { newParcel ->
|
||||
newParcel.writeInt(PARCEL_VAL_PARCELABLE)
|
||||
Parcel.obtain().use { oldParcel ->
|
||||
oldParcel.unmarshall(oldBytes, 0, oldBytes.size)
|
||||
oldParcel.setDataPosition(0)
|
||||
migratePath1_1_0(oldParcel, newParcel)
|
||||
}
|
||||
newParcel.marshall()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
|
||||
}
|
||||
|
||||
private fun migrateFileSortOptionsSetting1_1_0() {
|
||||
migrateFileSortOptionsSetting1_1_0(
|
||||
defaultSharedPreferences, application.getString(R.string.pref_key_file_list_sort_options)
|
||||
)
|
||||
}
|
||||
|
||||
private fun migrateFileSortOptionsSetting1_1_0(sharedPreferences: SharedPreferences, key: String) {
|
||||
val oldBytes = sharedPreferences.getString(key, null)?.asBase64()?.toByteArray() ?: return
|
||||
val newBytes = try {
|
||||
Parcel.obtain().use { newParcel ->
|
||||
newParcel.writeInt(PARCEL_VAL_PARCELABLE)
|
||||
Parcel.obtain().use { oldParcel ->
|
||||
oldParcel.unmarshall(oldBytes, 0, oldBytes.size)
|
||||
oldParcel.setDataPosition(0)
|
||||
newParcel.writeString(oldParcel.readString())
|
||||
newParcel.writeString(FileSortOptions.By.entries[oldParcel.readInt()].name)
|
||||
newParcel.writeString(FileSortOptions.Order.entries[oldParcel.readInt()].name)
|
||||
newParcel.writeInt(oldParcel.readByte().toInt())
|
||||
}
|
||||
newParcel.marshall()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
sharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
|
||||
}
|
||||
|
||||
fun migrateCreateArchiveTypeSetting1_1_0() {
|
||||
val key = application.getString(R.string.pref_key_create_archive_type)
|
||||
val oldValue = defaultSharedPreferences.getString(key, null) ?: return
|
||||
val newValue = oldValue.replace(Regex("type_.+$")) {
|
||||
when (it.value) {
|
||||
"type_zip" -> "zipRadio"
|
||||
"type_tar_xz" -> "tarXzRadio"
|
||||
"type_seven_z" -> "sevenZRadio"
|
||||
else -> "zipRadio"
|
||||
}
|
||||
}
|
||||
defaultSharedPreferences.edit { putString(key, newValue) }
|
||||
}
|
||||
|
||||
private fun migrateStandardDirectorySettingsSetting1_1_0() {
|
||||
val key = application.getString(R.string.pref_key_standard_directory_settings)
|
||||
val oldBytes = defaultSharedPreferences.getString(key, null)?.asBase64()?.toByteArray()
|
||||
?: return
|
||||
val newBytes = try {
|
||||
Parcel.obtain().use { newParcel ->
|
||||
newParcel.writeInt(PARCEL_VAL_LIST)
|
||||
Parcel.obtain().use { oldParcel ->
|
||||
oldParcel.unmarshall(oldBytes, 0, oldBytes.size)
|
||||
oldParcel.setDataPosition(0)
|
||||
val size = oldParcel.readInt()
|
||||
newParcel.writeInt(size)
|
||||
repeat(size) {
|
||||
oldParcel.readInt()
|
||||
newParcel.writeInt(PARCEL_VAL_PARCELABLE)
|
||||
newParcel.writeString(StandardDirectorySettings::class.java.name)
|
||||
newParcel.writeString(oldParcel.readString())
|
||||
newParcel.writeString(oldParcel.readString())
|
||||
newParcel.writeInt(oldParcel.readByte().toInt())
|
||||
}
|
||||
}
|
||||
newParcel.marshall()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
|
||||
}
|
||||
|
||||
private fun migrateBookmarkDirectoriesSetting1_1_0() {
|
||||
val key = application.getString(R.string.pref_key_bookmark_directories)
|
||||
val oldBytes = defaultSharedPreferences.getString(key, null)?.asBase64()?.toByteArray()
|
||||
?: return
|
||||
val newBytes = try {
|
||||
Parcel.obtain().use { newParcel ->
|
||||
newParcel.writeInt(PARCEL_VAL_LIST)
|
||||
Parcel.obtain().use { oldParcel ->
|
||||
oldParcel.unmarshall(oldBytes, 0, oldBytes.size)
|
||||
oldParcel.setDataPosition(0)
|
||||
val size = oldParcel.readInt()
|
||||
newParcel.writeInt(size)
|
||||
repeat(size) {
|
||||
oldParcel.readInt()
|
||||
newParcel.writeInt(PARCEL_VAL_PARCELABLE)
|
||||
newParcel.writeString(BookmarkDirectory::class.java.name)
|
||||
newParcel.writeLong(oldParcel.readLong())
|
||||
newParcel.writeString(oldParcel.readString())
|
||||
migratePath1_1_0(oldParcel, newParcel)
|
||||
}
|
||||
}
|
||||
newParcel.marshall()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
|
||||
}
|
||||
|
||||
private val oldByteStringCreator = object : Parcelable.Creator<ByteString> {
|
||||
override fun createFromParcel(source: Parcel): ByteString =
|
||||
source.createByteArray()!!.moveToByteString()
|
||||
|
||||
override fun newArray(size: Int): Array<ByteString?> = arrayOfNulls(size)
|
||||
}
|
||||
|
||||
private fun migratePath1_1_0(oldParcel: Parcel, newParcel: Parcel) {
|
||||
val className = oldParcel.readString()
|
||||
newParcel.writeString(className)
|
||||
newParcel.writeByte(oldParcel.readByte())
|
||||
newParcel.writeBooleanCompat(oldParcel.readByte() != 0.toByte())
|
||||
newParcel.writeParcelableListCompat(oldParcel.createTypedArrayList(oldByteStringCreator), 0)
|
||||
when (className) {
|
||||
"me.zhanghai.android.files.provider.archive.ArchivePath" -> {
|
||||
oldParcel.readString()
|
||||
newParcel.writeString(ArchiveFileSystem::class.java.name)
|
||||
migratePath1_1_0(oldParcel, newParcel)
|
||||
}
|
||||
"me.zhanghai.android.files.provider.content.ContentPath" -> {
|
||||
oldParcel.readString()
|
||||
newParcel.writeString(ContentFileSystem::class.java.name)
|
||||
newParcel.writeParcelable(oldParcel.readParcelable<Uri>(), 0)
|
||||
}
|
||||
"me.zhanghai.android.files.provider.document.DocumentPath" -> {
|
||||
oldParcel.readString()
|
||||
newParcel.writeString(DocumentFileSystem::class.java.name)
|
||||
newParcel.writeParcelable(oldParcel.readParcelable<Uri>(), 0)
|
||||
}
|
||||
"me.zhanghai.android.files.provider.linux.LinuxPath" -> {
|
||||
oldParcel.readString()
|
||||
newParcel.writeString(LinuxFileSystem::class.java.name)
|
||||
newParcel.writeBooleanCompat(oldParcel.readByte() != 0.toByte())
|
||||
}
|
||||
else -> throw IllegalStateException(className)
|
||||
}
|
||||
}
|
||||
|
||||
private val pathSharedPreferences: SharedPreferences
|
||||
get() {
|
||||
val name = "${PreferenceManagerCompat.getDefaultSharedPreferencesName(application)}_path"
|
||||
val mode = PreferenceManagerCompat.defaultSharedPreferencesMode
|
||||
return application.getSharedPreferences(name, mode)
|
||||
}
|
||||
|
||||
internal fun upgradeAppTo1_2_0() {
|
||||
migrateStoragesSetting1_2_0()
|
||||
}
|
||||
|
||||
private fun migrateStoragesSetting1_2_0() {
|
||||
val key = application.getString(R.string.pref_key_storages)
|
||||
val storages = (listOf(FileSystemRoot(null, true), PrimaryStorageVolume(null, true))
|
||||
+ DocumentTreeUri.persistedUris.map {
|
||||
DocumentTree(
|
||||
null, it.storageVolume?.getDescriptionCompat(application) ?: it.displayName
|
||||
?: it.value.toString(), it
|
||||
)
|
||||
})
|
||||
val bytes = Parcel.obtain().use { parcel ->
|
||||
parcel.writeValue(storages)
|
||||
parcel.marshall()
|
||||
}
|
||||
defaultSharedPreferences.edit { putString(key, bytes.toBase64().value) }
|
||||
}
|
||||
|
||||
internal fun upgradeAppTo1_3_0() {
|
||||
migrateSmbServersSetting1_3_0()
|
||||
}
|
||||
|
||||
private fun migrateSmbServersSetting1_3_0() {
|
||||
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 ->
|
||||
newParcel.writeInt(PARCEL_VAL_LIST)
|
||||
Parcel.obtain().use { oldParcel ->
|
||||
oldParcel.unmarshall(oldBytes, 0, oldBytes.size)
|
||||
oldParcel.setDataPosition(0)
|
||||
val size = oldParcel.readInt()
|
||||
newParcel.writeInt(size)
|
||||
repeat(size) {
|
||||
val oldPosition = oldParcel.dataPosition()
|
||||
oldParcel.readInt()
|
||||
val className = oldParcel.readString()
|
||||
if (className == "me.zhanghai.android.files.storage.SmbServer") {
|
||||
newParcel.writeInt(PARCEL_VAL_PARCELABLE)
|
||||
newParcel.writeString("me.zhanghai.android.files.storage.SmbServer")
|
||||
val id = oldParcel.readLong()
|
||||
newParcel.writeLong(id)
|
||||
val customName = oldParcel.readString()
|
||||
newParcel.writeString(customName)
|
||||
oldParcel.readString()
|
||||
newParcel.writeString(
|
||||
"me.zhanghai.android.files.provider.smb.client.Authority"
|
||||
)
|
||||
val authorityHost = oldParcel.readString()
|
||||
newParcel.writeString(authorityHost)
|
||||
val authorityPort = oldParcel.readInt()
|
||||
newParcel.writeInt(authorityPort)
|
||||
oldParcel.readString()
|
||||
newParcel.writeString(
|
||||
"me.zhanghai.android.files.provider.smb.client.Authentication"
|
||||
)
|
||||
val authenticationUsername = oldParcel.readString()
|
||||
newParcel.writeString(authenticationUsername)
|
||||
val authenticationDomain = oldParcel.readString()
|
||||
newParcel.writeString(authenticationDomain)
|
||||
val authenticationPassword = oldParcel.readString()
|
||||
newParcel.writeString(authenticationPassword)
|
||||
val relativePath = ""
|
||||
newParcel.writeString(relativePath)
|
||||
} else {
|
||||
oldParcel.setDataPosition(oldPosition)
|
||||
val storage = oldParcel.readValue(appClassLoader)
|
||||
newParcel.writeValue(storage)
|
||||
}
|
||||
}
|
||||
}
|
||||
newParcel.marshall()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
|
||||
}
|
||||
|
||||
internal fun upgradeAppTo1_4_0() {
|
||||
migratePathSetting1_4_0(R.string.pref_key_file_list_default_directory)
|
||||
migrateSftpServersSetting1_4_0()
|
||||
migrateBookmarkDirectoriesSetting1_4_0()
|
||||
migrateRootStrategySetting1_4_0()
|
||||
migratePathSetting1_4_0(R.string.pref_key_ftp_server_home_directory)
|
||||
}
|
||||
|
||||
private fun migratePathSetting1_4_0(@StringRes keyRes: Int) {
|
||||
val key = application.getString(keyRes)
|
||||
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())
|
||||
migratePath1_4_0(oldParcel, newParcel)
|
||||
}
|
||||
newParcel.marshall()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
|
||||
}
|
||||
|
||||
private fun migrateBookmarkDirectoriesSetting1_4_0() {
|
||||
val key = application.getString(R.string.pref_key_bookmark_directories)
|
||||
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())
|
||||
val size = oldParcel.readInt()
|
||||
newParcel.writeInt(size)
|
||||
repeat(size) {
|
||||
newParcel.writeInt(oldParcel.readInt())
|
||||
newParcel.writeString(oldParcel.readString())
|
||||
newParcel.writeLong(oldParcel.readLong())
|
||||
newParcel.writeString(oldParcel.readString())
|
||||
migratePath1_4_0(oldParcel, newParcel)
|
||||
}
|
||||
}
|
||||
newParcel.marshall()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
defaultSharedPreferences.edit { putString(key, newBytes?.toBase64()?.value) }
|
||||
}
|
||||
|
||||
private fun migratePath1_4_0(oldParcel: Parcel, newParcel: Parcel) {
|
||||
val className = oldParcel.readString()
|
||||
newParcel.writeString(className)
|
||||
newParcel.writeByte(oldParcel.readByte())
|
||||
newParcel.writeBooleanCompat(oldParcel.readBooleanCompat())
|
||||
newParcel.writeParcelableListCompat(oldParcel.readParcelableListCompat<ByteString>(), 0)
|
||||
when (className) {
|
||||
"me.zhanghai.android.files.provider.archive.ArchivePath" -> {
|
||||
newParcel.writeString(oldParcel.readString())
|
||||
migratePath1_4_0(oldParcel, newParcel)
|
||||
}
|
||||
"me.zhanghai.android.files.provider.content.ContentPath" -> {
|
||||
newParcel.writeParcelable(oldParcel.readParcelable<ContentFileSystem>(), 0)
|
||||
newParcel.writeParcelable(oldParcel.readParcelable<Uri>(), 0)
|
||||
}
|
||||
"me.zhanghai.android.files.provider.document.DocumentPath" ->
|
||||
newParcel.writeParcelable(oldParcel.readParcelable<DocumentFileSystem>(), 0)
|
||||
"me.zhanghai.android.files.provider.linux.LinuxPath" -> {
|
||||
newParcel.writeParcelable(oldParcel.readParcelable<LinuxFileSystem>(), 0)
|
||||
oldParcel.readBooleanCompat()
|
||||
}
|
||||
"me.zhanghai.android.files.provider.sftp.SftpPath" ->
|
||||
newParcel.writeParcelable(oldParcel.readParcelable<SftpFileSystem>(), 0)
|
||||
"me.zhanghai.android.files.provider.smb.SmbPath" ->
|
||||
newParcel.writeParcelable(oldParcel.readParcelable<SmbFileSystem>(), 0)
|
||||
else -> throw IllegalStateException(className)
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateSftpServersSetting1_4_0() {
|
||||
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())
|
||||
val size = oldParcel.readInt()
|
||||
newParcel.writeInt(size)
|
||||
repeat(size) {
|
||||
val oldPosition = oldParcel.dataPosition()
|
||||
oldParcel.readInt()
|
||||
when (oldParcel.readString()) {
|
||||
"me.zhanghai.android.files.storage.SftpServer" -> {
|
||||
newParcel.writeInt(PARCEL_VAL_PARCELABLE)
|
||||
newParcel.writeString("me.zhanghai.android.files.storage.SftpServer")
|
||||
val id = oldParcel.readLong()
|
||||
newParcel.writeLong(id)
|
||||
val customName = oldParcel.readString()
|
||||
newParcel.writeString(customName)
|
||||
val authorityHost = oldParcel.readString()
|
||||
newParcel.writeString(authorityHost)
|
||||
val authorityPort = oldParcel.readInt()
|
||||
newParcel.writeInt(authorityPort)
|
||||
val authenticationClassName = oldParcel.readString()
|
||||
val authorityUsername = oldParcel.readString()
|
||||
newParcel.writeString(authorityUsername)
|
||||
newParcel.writeString(authenticationClassName)
|
||||
val authenticationPasswordOrPrivateKey = oldParcel.readString()
|
||||
newParcel.writeString(authenticationPasswordOrPrivateKey)
|
||||
val relativePath = oldParcel.readString()
|
||||
newParcel.writeString(relativePath)
|
||||
}
|
||||
else -> {
|
||||
oldParcel.setDataPosition(oldPosition)
|
||||
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 migrateRootStrategySetting1_4_0() {
|
||||
val key = application.getString(R.string.pref_key_root_strategy)
|
||||
val oldValue = defaultSharedPreferences.getString(key, null)?.toInt() ?: return
|
||||
val newValue = when (oldValue) {
|
||||
0 -> RootStrategy.NEVER
|
||||
3 -> RootStrategy.ALWAYS
|
||||
else -> RootStrategy.AUTOMATIC
|
||||
}.ordinal.toString()
|
||||
defaultSharedPreferences.edit { putString(key, newValue) }
|
||||
}
|
||||
|
||||
internal fun upgradeAppTo1_5_0() {
|
||||
migrateSftpServersSetting1_5_0()
|
||||
}
|
||||
|
||||
private fun migrateSftpServersSetting1_5_0() {
|
||||
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())
|
||||
val size = oldParcel.readInt()
|
||||
newParcel.writeInt(size)
|
||||
repeat(size) {
|
||||
val oldPosition = oldParcel.dataPosition()
|
||||
oldParcel.readInt()
|
||||
when (oldParcel.readString()) {
|
||||
"me.zhanghai.android.files.storage.SftpServer" -> {
|
||||
newParcel.writeInt(PARCEL_VAL_PARCELABLE)
|
||||
newParcel.writeString("me.zhanghai.android.files.storage.SftpServer")
|
||||
val id = oldParcel.readLong()
|
||||
newParcel.writeLong(id)
|
||||
val customName = oldParcel.readString()
|
||||
newParcel.writeString(customName)
|
||||
val authorityHost = oldParcel.readString()
|
||||
newParcel.writeString(authorityHost)
|
||||
val authorityPort = oldParcel.readInt()
|
||||
newParcel.writeInt(authorityPort)
|
||||
val authorityUsername = oldParcel.readString()
|
||||
newParcel.writeString(authorityUsername)
|
||||
val authenticationClassName = oldParcel.readString()
|
||||
newParcel.writeString(authenticationClassName)
|
||||
val authenticationPasswordOrPrivateKey = oldParcel.readString()
|
||||
newParcel.writeString(authenticationPasswordOrPrivateKey)
|
||||
if (authenticationClassName == "me.zhanghai.android.files.provider.sftp"
|
||||
+ ".client.PublicKeyAuthentication") {
|
||||
newParcel.writeString(null)
|
||||
}
|
||||
val relativePath = oldParcel.readString()
|
||||
newParcel.writeString(relativePath)
|
||||
}
|
||||
else -> {
|
||||
oldParcel.setDataPosition(oldPosition)
|
||||
val storage = oldParcel.readValue(appClassLoader)
|
||||
newParcel.writeValue(storage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newParcel.marshall()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.app
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import me.zhanghai.android.files.R
|
||||
import me.zhanghai.android.files.util.NotificationChannelTemplate
|
||||
import me.zhanghai.android.files.util.NotificationTemplate
|
||||
import me.zhanghai.android.files.util.startActivitySafe
|
||||
|
||||
val backgroundActivityStartNotificationTemplate =
|
||||
NotificationTemplate(
|
||||
NotificationChannelTemplate(
|
||||
"background_activity_start",
|
||||
R.string.notification_channel_background_activity_start_name,
|
||||
NotificationManagerCompat.IMPORTANCE_HIGH,
|
||||
descriptionRes = R.string.notification_channel_background_activity_start_description,
|
||||
showBadge = false
|
||||
),
|
||||
colorRes = R.color.color_primary,
|
||||
smallIcon = R.drawable.notification_icon,
|
||||
ongoing = true,
|
||||
autoCancel = true,
|
||||
category = NotificationCompat.CATEGORY_ERROR,
|
||||
priority = NotificationCompat.PRIORITY_HIGH
|
||||
)
|
||||
|
||||
object BackgroundActivityStarter {
|
||||
fun startActivity(intent: Intent, title: CharSequence, text: CharSequence?, context: Context) {
|
||||
// TODO: Only use new task when in background?
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
if (isInForeground) {
|
||||
context.startActivitySafe(intent)
|
||||
} else {
|
||||
notifyStartActivity(intent, title, text, context)
|
||||
}
|
||||
}
|
||||
|
||||
private val isInForeground: Boolean
|
||||
get() = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(
|
||||
Lifecycle.State.STARTED
|
||||
)
|
||||
|
||||
private fun notifyStartActivity(
|
||||
intent: Intent,
|
||||
title: CharSequence,
|
||||
text: CharSequence?,
|
||||
context: Context
|
||||
) {
|
||||
var pendingIntentFlags = PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_CANCEL_CURRENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
pendingIntentFlags = pendingIntentFlags or PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context, intent.hashCode(), intent, pendingIntentFlags
|
||||
)
|
||||
val notification = backgroundActivityStartNotificationTemplate.createBuilder(context)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
notificationManager.notify(intent.hashCode(), notification)
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.app
|
||||
|
||||
object NotificationIds {
|
||||
const val FTP_SERVER = 1
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.app
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.PowerManager
|
||||
import android.os.storage.StorageManager
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
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 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)
|
||||
}
|
||||
|
||||
val storageManager: StorageManager by lazy {
|
||||
application.getSystemServiceCompat(StorageManager::class.java)
|
||||
}
|
||||
|
||||
val wifiManager: WifiManager by lazy {
|
||||
application.getSystemServiceCompat(WifiManager::class.java)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
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 AppIconApplicationInfoKeyer : Keyer<ApplicationInfo> {
|
||||
override fun key(data: ApplicationInfo, options: Options): String =
|
||||
AppIconLoader.getIconKey(data, data.longVersionCodeCompat, options.context)
|
||||
}
|
||||
|
||||
class AppIconApplicationInfoFetcherFactory(
|
||||
context: Context
|
||||
) : AppIconFetcher.Factory<ApplicationInfo>(
|
||||
// This is used by PrincipalListAdapter.
|
||||
context.getDimensionPixelSize(R.dimen.icon_size), context
|
||||
) {
|
||||
override fun getApplicationInfo(data: ApplicationInfo): Pair<ApplicationInfo, Closeable?> =
|
||||
data to null
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.fetch.DrawableResult
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.request.Options
|
||||
import me.zhanghai.android.appiconloader.AppIconLoader
|
||||
import java.io.Closeable
|
||||
|
||||
class AppIconFetcher(
|
||||
private val options: Options,
|
||||
private val appIconLoader: AppIconLoader,
|
||||
private val getApplicationInfo: () -> Pair<ApplicationInfo, Closeable?>
|
||||
) : 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(options.context.resources), false, DataSource.DISK)
|
||||
}
|
||||
|
||||
abstract class Factory<T : Any>(
|
||||
iconSize: Int,
|
||||
context: Context,
|
||||
shrinkNonAdaptiveIcons: Boolean = false
|
||||
) : Fetcher.Factory<T> {
|
||||
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<ApplicationInfo, Closeable?>
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
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
|
||||
import java.io.Closeable
|
||||
|
||||
data class AppIconPackageName(val packageName: String)
|
||||
|
||||
class AppIconPackageNameKeyer : Keyer<AppIconPackageName> {
|
||||
override fun key(data: AppIconPackageName, options: Options): String = data.packageName
|
||||
}
|
||||
|
||||
class AppIconPackageNameFetcherFactory(
|
||||
private val context: Context
|
||||
) : AppIconFetcher.Factory<AppIconPackageName>(
|
||||
// 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 getApplicationInfo(data: AppIconPackageName): Pair<ApplicationInfo, Closeable?> {
|
||||
// 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
|
||||
// managed profile. It may also help corner cases like when the package is hidden.
|
||||
return context.packageManager.getApplicationInfo(
|
||||
data.packageName, PackageManagerCompat.MATCH_UNINSTALLED_PACKAGES
|
||||
) to null
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.coil
|
||||
|
||||
import coil.request.ImageRequest
|
||||
import coil.transition.CrossfadeTransition
|
||||
|
||||
fun ImageRequest.Builder.fadeIn(durationMillis: Int): ImageRequest.Builder =
|
||||
apply {
|
||||
placeholder(android.R.color.transparent)
|
||||
transitionFactory(CrossfadeTransition.Factory(durationMillis, true))
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.coil
|
||||
|
||||
import android.os.Build
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import me.zhanghai.android.files.app.application
|
||||
|
||||
fun initializeCoil() {
|
||||
Coil.setImageLoader(
|
||||
ImageLoader.Builder(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.Factory()
|
||||
} else {
|
||||
GifDecoder.Factory()
|
||||
}
|
||||
)
|
||||
add(SvgDecoder.Factory(false))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* 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
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.coil
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import coil.request.ImageRequest
|
||||
import coil.target.ImageViewTarget
|
||||
|
||||
// Setting the placeholder drawable as error drawable again causes animation glitches, so we just
|
||||
// ignore the onError() callback.
|
||||
fun ImageRequest.Builder.ignoreError() {
|
||||
val view = (build().target as ImageViewTarget).view
|
||||
target(object : ImageViewTarget(view) {
|
||||
override fun onError(error: Drawable?) {}
|
||||
})
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
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.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
|
||||
import me.zhanghai.android.files.compat.use
|
||||
import me.zhanghai.android.files.file.MimeType
|
||||
import me.zhanghai.android.files.file.asMimeType
|
||||
import me.zhanghai.android.files.file.isApk
|
||||
import me.zhanghai.android.files.file.isImage
|
||||
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
|
||||
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.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
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import me.zhanghai.android.files.util.setDataSource as appSetDataSource
|
||||
|
||||
class PathAttributesKeyer : Keyer<Pair<Path, BasicFileAttributes>> {
|
||||
override fun key(data: Pair<Path, BasicFileAttributes>, options: Options): String {
|
||||
val (path, attributes) = data
|
||||
return "$path:${attributes.lastModifiedInstant.toEpochMilli()}"
|
||||
}
|
||||
}
|
||||
|
||||
class PathAttributesFetcher(
|
||||
private val data: Pair<Path, BasicFileAttributes>,
|
||||
private val options: Options,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val appIconFetcherFactory: AppIconFetcher.Factory<Path>,
|
||||
private val videoFrameFetcherFactory: VideoFrameFetcher.Factory<Path>,
|
||||
private val pdfPageFetcherFactory: PdfPageFetcher.Factory<Path>
|
||||
) : Fetcher {
|
||||
override suspend fun fetch(): FetchResult? {
|
||||
val (path, attributes) = data
|
||||
val (width, height) = options.size
|
||||
// @see android.provider.MediaStore.ThumbnailConstants.MINI_SIZE
|
||||
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) {
|
||||
val thumbnail = runWithCancellationSignal { signal ->
|
||||
try {
|
||||
DocumentResolver.getThumbnail(
|
||||
path as DocumentResolver.Path, width.px, height.px, signal
|
||||
)
|
||||
} catch (e: ResolverException) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
if (thumbnail != null) {
|
||||
return DrawableResult(
|
||||
thumbnail.toDrawable(options.context.resources), true, path.dataSource
|
||||
)
|
||||
}
|
||||
}
|
||||
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 appIconFetcherFactory.create(path, options, imageLoader).fetch()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
mimeType.isImage || mimeType == MimeType.GENERIC -> {
|
||||
val inputStream = path.newInputStream()
|
||||
return SourceResult(
|
||||
ImageSource(inputStream.source().buffer(), options.context),
|
||||
if (mimeType != MimeType.GENERIC) mimeType.value else null, path.dataSource
|
||||
)
|
||||
}
|
||||
mimeType.isMedia && path.isMediaMetadataRetrieverCompatible -> {
|
||||
val embeddedPicture = try {
|
||||
MediaMetadataRetriever().use { retriever ->
|
||||
retriever.setDataSource(path)
|
||||
retriever.embeddedPicture
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
if (embeddedPicture != null) {
|
||||
return SourceResult(
|
||||
ImageSource(
|
||||
embeddedPicture.inputStream().source().buffer(), options.context
|
||||
), null, path.dataSource
|
||||
)
|
||||
}
|
||||
if (mimeType.isVideo) {
|
||||
try {
|
||||
return videoFrameFetcherFactory.create(path, options, imageLoader).fetch()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
mimeType.isPdf && (path.isLinuxPath || path.isDocumentPath) -> {
|
||||
try {
|
||||
return pdfPageFetcherFactory.create(path, options, imageLoader).fetch()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
class Factory(private val context: Context) : Fetcher.Factory<Pair<Path, BasicFileAttributes>> {
|
||||
private val appIconFetcherFactory = object : AppIconFetcher.Factory<Path>(
|
||||
// This is used by FileListAdapter.
|
||||
context.getDimensionPixelSize(R.dimen.large_icon_size), context
|
||||
) {
|
||||
override fun getApplicationInfo(data: Path): Pair<ApplicationInfo, Closeable?> {
|
||||
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<Path>() {
|
||||
override fun MediaMetadataRetriever.setDataSource(data: Path) {
|
||||
appSetDataSource(data)
|
||||
}
|
||||
}
|
||||
|
||||
private val pdfPageFetcherFactory = object : PdfPageFetcher.Factory<Path>() {
|
||||
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<Path, BasicFileAttributes>,
|
||||
options: Options,
|
||||
imageLoader: ImageLoader
|
||||
): Fetcher =
|
||||
PathAttributesFetcher(
|
||||
data, options, imageLoader, appIconFetcherFactory, videoFrameFetcherFactory,
|
||||
pdfPageFetcherFactory
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.coil
|
||||
|
||||
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.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.DecodeUtils
|
||||
import coil.fetch.DrawableResult
|
||||
import coil.fetch.FetchResult
|
||||
import coil.fetch.Fetcher
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.Options
|
||||
import coil.request.Parameters
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
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." }
|
||||
return setParameter(PdfPageFetcher.PDF_PAGE_INDEX_KEY, pageIndex)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun Parameters.pdfBackgroundColor(): Int? = value(PdfPageFetcher.PDF_BACKGROUND_COLOR_KEY) as Int?
|
||||
|
||||
@IntRange(from = 0)
|
||||
fun Parameters.pdfPageIndex(): Int? = value(PdfPageFetcher.PDF_PAGE_INDEX_KEY) as Int?
|
||||
|
||||
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"
|
||||
}
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<T : Any> : Fetcher.Factory<T> {
|
||||
override fun create(data: T, options: Options, imageLoader: ImageLoader): Fetcher =
|
||||
PdfPageFetcher(options) { openParcelFileDescriptor(data) }
|
||||
|
||||
protected abstract fun openParcelFileDescriptor(data: T): ParcelFileDescriptor
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* 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 || 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<T : Any> : Fetcher.Factory<T> {
|
||||
override fun create(data: T, options: Options, imageLoader: ImageLoader): Fetcher =
|
||||
VideoFrameFetcher(options) { setDataSource(data) }
|
||||
|
||||
protected abstract fun MediaMetadataRetriever.setDataSource(data: T)
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.colorpicker
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.preference.DialogPreference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import me.zhanghai.android.files.R
|
||||
import me.zhanghai.android.files.util.getFloatByAttr
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
abstract class BaseColorPreference : DialogPreference {
|
||||
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 {
|
||||
widgetLayoutResource = R.layout.color_preference_widget
|
||||
dialogLayoutResource = R.layout.color_picker_dialog
|
||||
setPositiveButtonText(android.R.string.ok)
|
||||
setNegativeButtonText(android.R.string.cancel)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
val swatchView = holder.findViewById(R.id.swatch)
|
||||
if (swatchView != null) {
|
||||
val swatchDrawable = swatchView.background as GradientDrawable
|
||||
swatchDrawable.setColor(value)
|
||||
var alpha = 0xFF
|
||||
if (!isEnabled) {
|
||||
val disabledAlpha = context.getFloatByAttr(android.R.attr.disabledAlpha)
|
||||
alpha = (disabledAlpha * alpha).roundToInt()
|
||||
}
|
||||
swatchDrawable.alpha = alpha
|
||||
}
|
||||
}
|
||||
|
||||
@get:ColorInt
|
||||
abstract var value: Int
|
||||
|
||||
@get:ColorInt
|
||||
abstract val defaultValue: Int
|
||||
|
||||
abstract val entryValues: IntArray
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.colorpicker
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AbsListView
|
||||
import android.widget.BaseAdapter
|
||||
|
||||
class ColorPaletteAdapter(private val colors: IntArray) : BaseAdapter() {
|
||||
override fun hasStableIds(): Boolean = true
|
||||
|
||||
override fun getCount(): Int = colors.size
|
||||
|
||||
override fun getItem(position: Int): Int = colors[position]
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).toLong()
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val swatchView = convertView as ColorSwatchView?
|
||||
?: ColorSwatchView(parent.context).apply {
|
||||
layoutParams = AbsListView.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
return swatchView.apply {
|
||||
setColor(getItem(position))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.colorpicker
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.GridView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.ViewCompat
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import me.zhanghai.android.files.R
|
||||
import me.zhanghai.android.files.ui.MaterialPreferenceDialogFragmentCompat
|
||||
import me.zhanghai.android.files.util.ParcelableState
|
||||
import me.zhanghai.android.files.util.getState
|
||||
import me.zhanghai.android.files.util.putState
|
||||
import me.zhanghai.android.files.util.withTheme
|
||||
|
||||
class ColorPreferenceDialogFragment : MaterialPreferenceDialogFragmentCompat() {
|
||||
override val preference: BaseColorPreference
|
||||
get() = super.preference as BaseColorPreference
|
||||
|
||||
private lateinit var colors: IntArray
|
||||
private var checkedColor = 0
|
||||
private var defaultColor = 0
|
||||
|
||||
private lateinit var paletteGrid: GridView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val preference = preference
|
||||
colors = preference.entryValues
|
||||
checkedColor = preference.value
|
||||
defaultColor = preference.defaultValue
|
||||
} else {
|
||||
val state = savedInstanceState.getState<State>()
|
||||
colors = state.colors
|
||||
checkedColor = state.checkedColor
|
||||
defaultColor = state.defaultColor
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
val checkedPosition = paletteGrid.checkedItemPosition
|
||||
val checkedColor = if (checkedPosition != -1) colors[checkedPosition] else checkedColor
|
||||
outState.putState(State(colors, checkedColor, defaultColor))
|
||||
}
|
||||
|
||||
override fun onCreateDialogView(context: Context): View? =
|
||||
super.onCreateDialogView(context.withTheme(theme))
|
||||
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
|
||||
paletteGrid = ViewCompat.requireViewById(view, R.id.palette)
|
||||
paletteGrid.adapter = ColorPaletteAdapter(colors)
|
||||
val checkedPosition = colors.indexOf(checkedColor)
|
||||
if (checkedPosition != -1) {
|
||||
paletteGrid.setItemChecked(checkedPosition, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareDialogBuilder(builder: AlertDialog.Builder) {
|
||||
super.onPrepareDialogBuilder(builder)
|
||||
|
||||
if (defaultColor in colors) {
|
||||
builder.setNeutralButton(R.string.default_, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
(super.onCreateDialog(savedInstanceState) as AlertDialog).apply {
|
||||
if (defaultColor in colors) {
|
||||
// Override the listener here so that we won't close the dialog.
|
||||
setOnShowListener {
|
||||
getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
|
||||
paletteGrid.setItemChecked(colors.indexOf(defaultColor), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogClosed(positiveResult: Boolean) {
|
||||
if (!positiveResult) {
|
||||
return
|
||||
}
|
||||
val checkedPosition = paletteGrid.checkedItemPosition
|
||||
if (checkedPosition == -1) {
|
||||
return
|
||||
}
|
||||
val checkedColor = colors[checkedPosition]
|
||||
preference.value = checkedColor
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
private class State(
|
||||
val colors: IntArray,
|
||||
val checkedColor: Int,
|
||||
val defaultColor: Int
|
||||
) : ParcelableState
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.colorpicker
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import me.zhanghai.android.files.R
|
||||
import me.zhanghai.android.files.ui.CheckableView
|
||||
|
||||
class ColorSwatchView : CheckableView {
|
||||
private val gradientDrawable: GradientDrawable
|
||||
|
||||
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 {
|
||||
val background = AppCompatResources.getDrawable(
|
||||
context, R.drawable.color_swatch_view_background
|
||||
) as LayerDrawable
|
||||
gradientDrawable = background.getDrawable(0) as GradientDrawable
|
||||
setBackground(background)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
setMeasuredDimension(
|
||||
resolveSize(suggestedMinimumWidth, widthMeasureSpec),
|
||||
resolveSize(suggestedMinimumHeight, heightMeasureSpec)
|
||||
)
|
||||
}
|
||||
|
||||
fun setColor(@ColorInt color: Int) {
|
||||
gradientDrawable.apply {
|
||||
mutate()
|
||||
setColor(color)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager.TaskDescription
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.app.ActivityCompat
|
||||
import me.zhanghai.android.files.util.getColorByAttr
|
||||
|
||||
fun Activity.recreateCompat() {
|
||||
ActivityCompat.recreate(this)
|
||||
}
|
||||
|
||||
fun Activity.setThemeCompat(@StyleRes resid: Int) {
|
||||
setTheme(resid)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val surfaceColor = getColorByAttr(com.google.android.material.R.attr.colorSurface)
|
||||
if (surfaceColor != 0 && Color.alpha(surfaceColor) == 0xFF) {
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(TaskDescription(null, null, surfaceColor))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Build
|
||||
import me.zhanghai.android.files.hiddenapi.RestrictedHiddenApi
|
||||
import me.zhanghai.android.files.util.lazyReflectedField
|
||||
|
||||
@RestrictedHiddenApi
|
||||
private val versionCodeField by lazyReflectedField(ApplicationInfo::class.java, "versionCode")
|
||||
|
||||
@RestrictedHiddenApi
|
||||
private val longVersionCodeField by lazyReflectedField(
|
||||
ApplicationInfo::class.java, "longVersionCode"
|
||||
)
|
||||
|
||||
val ApplicationInfo.longVersionCodeCompat: Long
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
longVersionCodeField.getLong(this)
|
||||
} else {
|
||||
versionCodeField.getInt(this).toLong()
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
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
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.annotation.StyleableRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.TintTypedArray
|
||||
import androidx.core.content.ContextCompat
|
||||
import me.zhanghai.android.files.hiddenapi.RestrictedHiddenApi
|
||||
import me.zhanghai.android.files.util.lazyReflectedMethod
|
||||
import java.util.concurrent.Executor
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
fun Context.checkSelfPermissionCompat(permission: String): Int =
|
||||
ContextCompat.checkSelfPermission(this, permission)
|
||||
|
||||
@ColorInt
|
||||
fun Context.getColorCompat(@ColorRes id: Int): Int = getColorStateListCompat(id).defaultColor
|
||||
|
||||
fun Context.getColorStateListCompat(@ColorRes id: Int): ColorStateList =
|
||||
AppCompatResources.getColorStateList(this, id)!!
|
||||
|
||||
fun Context.getDrawableCompat(@DrawableRes id: Int): Drawable =
|
||||
AppCompatResources.getDrawable(this, id)!!
|
||||
|
||||
fun <T> Context.getSystemServiceCompat(serviceClass: Class<T>): T =
|
||||
ContextCompat.getSystemService(this, serviceClass)!!
|
||||
|
||||
val Context.mainExecutorCompat: Executor
|
||||
get() = ContextCompat.getMainExecutor(this)
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun Context.obtainStyledAttributesCompat(
|
||||
set: AttributeSet? = null,
|
||||
@StyleableRes attrs: IntArray,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
@StyleRes defStyleRes: Int = 0
|
||||
): TintTypedArray =
|
||||
TintTypedArray.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes)
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
@SuppressLint("RestrictedApi")
|
||||
inline fun <R> TintTypedArray.use(block: (TintTypedArray) -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
return try {
|
||||
block(this)
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
val Context.themeResIdCompat: Int
|
||||
@StyleRes
|
||||
get() = getThemeResIdMethod.invoke(this) as Int
|
|
@ -1,12 +0,0 @@
|
|||
package me.zhanghai.android.files.compat
|
||||
|
||||
import org.threeten.bp.DateTimeUtils
|
||||
import org.threeten.bp.Instant
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
fun Calendar.toInstantCompat(): Instant = DateTimeUtils.toInstant(this)
|
||||
|
||||
fun Date.toInstantCompat(): Instant = DateTimeUtils.toInstant(this)
|
||||
|
||||
fun Instant.toDateCompat(): Date = DateTimeUtils.toDate(this)
|
|
@ -1,15 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.app.Dialog
|
||||
import android.view.View
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.app.DialogCompat
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : View> Dialog.requireViewByIdCompat(@IdRes id: Int): T =
|
||||
DialogCompat.requireViewById(this, id) as T
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.os.Build
|
||||
import android.text.method.DigitsKeyListener
|
||||
import java.util.Locale
|
||||
|
||||
object DigitsKeyListenerCompat {
|
||||
fun getInstance(locale: Locale?, sign: Boolean, decimal: Boolean): DigitsKeyListener =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
DigitsKeyListener.getInstance(locale, sign, decimal)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
DigitsKeyListener.getInstance(sign, decimal)
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
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"
|
||||
const val EXTRA_SHOW_ADVANCED = "android.provider.extra.SHOW_ADVANCED"
|
||||
|
||||
const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
|
||||
const val EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID = "primary"
|
||||
|
||||
private const val PATH_DOCUMENT = "document"
|
||||
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) {
|
||||
return false
|
||||
}
|
||||
val pathSegments = uri.pathSegments
|
||||
return when (pathSegments.size) {
|
||||
2 -> pathSegments[0] == PATH_DOCUMENT
|
||||
4 -> pathSegments[0] == PATH_TREE && pathSegments[2] == PATH_DOCUMENT
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun isTreeUri(uri: Uri): Boolean =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
DocumentsContract.isTreeUri(uri)
|
||||
} else {
|
||||
uri.pathSegments.let { it.size >= 2 && it[0] == PATH_TREE }
|
||||
}
|
||||
|
||||
fun isChildDocumentsUri(uri: Uri): Boolean {
|
||||
val pathSegments = uri.pathSegments
|
||||
return when (pathSegments.size) {
|
||||
3 -> pathSegments[0] == PATH_DOCUMENT && pathSegments[2] == PATH_CHILDREN
|
||||
5 ->
|
||||
pathSegments[0] == PATH_TREE && pathSegments[2] == PATH_DOCUMENT
|
||||
&& pathSegments[4] == PATH_CHILDREN
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
|
||||
var Drawable.layoutDirectionCompat: Int
|
||||
get() = DrawableCompat.getLayoutDirection(this)
|
||||
set(value) {
|
||||
DrawableCompat.setLayoutDirection(this, value)
|
||||
}
|
||||
|
||||
fun Drawable.setTintCompat(@ColorInt tint: Int) {
|
||||
DrawableCompat.setTint(this, tint)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
object EnvironmentCompat2 {
|
||||
/**
|
||||
* @see android.os.Environment.DIRECTORY_SCREENSHOTS
|
||||
*/
|
||||
const val DIRECTORY_SCREENSHOTS = "Screenshots"
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.system.ErrnoException
|
||||
import me.zhanghai.android.files.hiddenapi.RestrictedHiddenApi
|
||||
import me.zhanghai.android.files.util.lazyReflectedField
|
||||
|
||||
@RestrictedHiddenApi
|
||||
private val functionNameField by lazyReflectedField(ErrnoException::class.java, "functionName")
|
||||
|
||||
val ErrnoException.functionNameCompat: String
|
||||
get() = functionNameField.get(this) as String
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun KClass<InputStream>.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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.content.Intent
|
||||
import me.zhanghai.android.files.util.andInv
|
||||
|
||||
fun Intent.removeFlagsCompat(flags: Int) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
removeFlags(flags)
|
||||
} else {
|
||||
setFlags(this.flags andInv flags)
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import kotlin.comparisons.reversed as kotlinReversed
|
||||
|
||||
fun <T> Comparator<T>.reversedCompat(): Comparator<T> = kotlinReversed()
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.icu.text.ListFormatter
|
||||
import android.os.Build
|
||||
|
||||
object ListFormatterCompat {
|
||||
fun format(vararg items: Any?): String =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ListFormatter.getInstance().format(*items)
|
||||
} else {
|
||||
formatCompat(items.asList())
|
||||
}
|
||||
|
||||
fun format(items: Collection<*>): String =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ListFormatter.getInstance().format(items)
|
||||
} else {
|
||||
formatCompat(items)
|
||||
}
|
||||
|
||||
private fun formatCompat(items: Collection<*>): String = items.joinToString(", ")
|
||||
}
|
|
@ -1,210 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* 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 <application>.
|
||||
*/
|
||||
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<String>()
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
val KClass<MediaMetadataRetriever>.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 <R> MediaMetadataRetriever.use(block: (MediaMetadataRetriever) -> R): R {
|
||||
contract {
|
||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
val autoCloseable: AutoCloseable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
this
|
||||
} else {
|
||||
AutoCloseable { release() }
|
||||
}
|
||||
return autoCloseable.use { block(this) }
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* 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)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.os.Build
|
||||
import android.system.OsConstants
|
||||
import me.zhanghai.android.files.hiddenapi.RestrictedHiddenApi
|
||||
import me.zhanghai.android.files.util.lazyReflectedMethod
|
||||
import java.io.Closeable
|
||||
import java.io.FileDescriptor
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
object NioUtilsCompat {
|
||||
@RestrictedHiddenApi
|
||||
private val newFileChannelMethod by lazyReflectedMethod(
|
||||
"java.nio.NioUtils", "newFileChannel", Closeable::class.java, FileDescriptor::class.java,
|
||||
Int::class.java
|
||||
)
|
||||
@RestrictedHiddenApi
|
||||
private val fileChannelImplOpenMethod by lazyReflectedMethod(
|
||||
"sun.nio.ch.FileChannelImpl", "open", FileDescriptor::class.java, String::class.java,
|
||||
Boolean::class.java, Boolean::class.java, Boolean::class.java, Any::class.java
|
||||
)
|
||||
|
||||
fun newFileChannel(ioObject: Closeable, fd: FileDescriptor, flags: Int): FileChannel =
|
||||
if (Build.VERSION.SDK_INT in Build.VERSION_CODES.N..<Build.VERSION_CODES.R) {
|
||||
// They broke O_RDONLY by assuming it's non-zero, but in fact it is zero.
|
||||
// https://android.googlesource.com/platform/libcore/+/nougat-release/luni/src/main/java/java/nio/NioUtils.java#63
|
||||
val readable = flags and OsConstants.O_ACCMODE != OsConstants.O_WRONLY
|
||||
val writable = flags and OsConstants.O_ACCMODE != OsConstants.O_RDONLY
|
||||
val append = flags and OsConstants.O_APPEND == OsConstants.O_APPEND
|
||||
fileChannelImplOpenMethod.invoke(
|
||||
null, fd, null, readable, writable, append, ioObject
|
||||
) as FileChannel
|
||||
} else {
|
||||
newFileChannelMethod.invoke(null, ioObject, fd, flags) as FileChannel
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
|
||||
val PackageInfo.longVersionCodeCompat: Long
|
||||
get() = PackageInfoCompat.getLongVersionCode(this)
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.SigningInfo
|
||||
import android.os.Build
|
||||
import me.zhanghai.android.files.util.andInv
|
||||
import me.zhanghai.android.files.util.hasBits
|
||||
|
||||
object PackageManagerCompat {
|
||||
@SuppressLint("InlinedApi")
|
||||
const val MATCH_UNINSTALLED_PACKAGES = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
}
|
||||
|
||||
fun PackageManager.getPackageArchiveInfoCompat(archiveFilePath: String, flags: Int): PackageInfo? {
|
||||
var packageInfo = getPackageArchiveInfo(archiveFilePath, flags)
|
||||
// getPackageArchiveInfo() returns null for unsigned APKs if signing info is requested.
|
||||
if (packageInfo == null) {
|
||||
val flagsWithoutGetSigningInfo = flags.andInv(
|
||||
@Suppress("DEPRECATION")
|
||||
PackageManager.GET_SIGNATURES or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
PackageManager.GET_SIGNING_CERTIFICATES
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
if (flags != flagsWithoutGetSigningInfo) {
|
||||
packageInfo = getPackageArchiveInfo(archiveFilePath, flagsWithoutGetSigningInfo)
|
||||
?.apply {
|
||||
@Suppress("DEPRECATION")
|
||||
if (flags.hasBits(PackageManager.GET_SIGNATURES)) {
|
||||
signatures = emptyArray()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& flags.hasBits(PackageManager.GET_SIGNING_CERTIFICATES)) {
|
||||
signingInfo = SigningInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return packageInfo
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.core.os.ParcelCompat
|
||||
|
||||
fun Parcel.readBooleanCompat(): Boolean = ParcelCompat.readBoolean(this)
|
||||
|
||||
fun Parcel.writeBooleanCompat(value: Boolean) {
|
||||
ParcelCompat.writeBoolean(this, value)
|
||||
}
|
||||
|
||||
fun <E : Parcelable?, L : MutableList<E>> Parcel.readParcelableListCompat(
|
||||
list: L,
|
||||
classLoader: ClassLoader?
|
||||
): L {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return readParcelableList(list, classLoader) as L
|
||||
} else {
|
||||
val size = readInt()
|
||||
if (size == -1) {
|
||||
list.clear()
|
||||
return list
|
||||
}
|
||||
val listSize = list.size
|
||||
for (index in 0..<size) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val element = readParcelable<E>(classLoader) as E
|
||||
if (index < listSize) {
|
||||
list[index] = element
|
||||
} else {
|
||||
list += element
|
||||
}
|
||||
}
|
||||
if (size < listSize) {
|
||||
list.subList(size, listSize).clear()
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Parcelable?> Parcel.writeParcelableListCompat(value: List<T>?, flags: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
writeParcelableList(value, flags)
|
||||
} else {
|
||||
if (value == null) {
|
||||
writeInt(-1)
|
||||
return
|
||||
}
|
||||
writeInt(value.size)
|
||||
for (element in value) {
|
||||
writeParcelable(element, flags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> Parcel.readSerializableCompat(): T? = readSerializable() as T?
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.content.pm.PermissionInfo
|
||||
import androidx.core.content.pm.PermissionInfoCompat
|
||||
|
||||
val PermissionInfo.protectionCompat: Int
|
||||
get() = PermissionInfoCompat.getProtection(this)
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object PreferenceManagerCompat {
|
||||
fun getDefaultSharedPreferencesName(context: Context): String =
|
||||
"${context.packageName}_preferences"
|
||||
|
||||
val defaultSharedPreferencesMode: Int
|
||||
get() = Context.MODE_PRIVATE
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.os.Build
|
||||
import android.os.ProxyFileDescriptorCallback
|
||||
import android.system.ErrnoException
|
||||
import android.system.OsConstants
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
abstract class ProxyFileDescriptorCallbackCompat {
|
||||
@Throws(ErrnoException::class)
|
||||
open fun onGetSize(): Long {
|
||||
throw ErrnoException("onGetSize", OsConstants.EBADF)
|
||||
}
|
||||
|
||||
@Throws(ErrnoException::class)
|
||||
open fun onRead(offset: Long, size: Int, data: ByteArray): Int {
|
||||
throw ErrnoException("onRead", OsConstants.EBADF)
|
||||
}
|
||||
|
||||
@Throws(ErrnoException::class)
|
||||
open fun onWrite(offset: Long, size: Int, data: ByteArray): Int {
|
||||
throw ErrnoException("onWrite", OsConstants.EBADF)
|
||||
}
|
||||
|
||||
@Throws(ErrnoException::class)
|
||||
open fun onFsync() {
|
||||
throw ErrnoException("onFsync", OsConstants.EINVAL)
|
||||
}
|
||||
|
||||
abstract fun onRelease()
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun toProxyFileDescriptorCallback(): ProxyFileDescriptorCallback {
|
||||
return object : ProxyFileDescriptorCallback() {
|
||||
@Throws(ErrnoException::class)
|
||||
override fun onGetSize(): Long = this@ProxyFileDescriptorCallbackCompat.onGetSize()
|
||||
|
||||
@Throws(ErrnoException::class)
|
||||
override fun onRead(offset: Long, size: Int, data: ByteArray): Int =
|
||||
this@ProxyFileDescriptorCallbackCompat.onRead(offset, size, data)
|
||||
|
||||
@Throws(ErrnoException::class)
|
||||
override fun onWrite(offset: Long, size: Int, data: ByteArray): Int =
|
||||
this@ProxyFileDescriptorCallbackCompat.onWrite(offset, size, data)
|
||||
|
||||
@Throws(ErrnoException::class)
|
||||
override fun onFsync() = this@ProxyFileDescriptorCallbackCompat.onFsync()
|
||||
|
||||
override fun onRelease() = this@ProxyFileDescriptorCallbackCompat.onRelease()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.DimenRes
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
|
||||
fun Resources.getFloatCompat(@DimenRes id: Int) = ResourcesCompat.getFloat(this, id)
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.graphics.drawable.RotateDrawable
|
||||
import android.os.Build
|
||||
import me.zhanghai.android.files.util.lazyReflectedField
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun KClass<RotateDrawable>.createCompat(): RotateDrawable =
|
||||
RotateDrawable().apply {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
isPivotXRelativeCompat = true
|
||||
pivotXCompat = 0.5f
|
||||
isPivotYRelativeCompat = true
|
||||
pivotYCompat = 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
private val rotateDrawableMStateField by lazyReflectedField(RotateDrawable::class.java, "mState")
|
||||
private val ROTATE_STATE_CLASS_NAME = "${RotateDrawable::class.java.name}\$RotateState"
|
||||
private val rotateStateMPivotXRelField by lazyReflectedField(ROTATE_STATE_CLASS_NAME, "mPivotXRel")
|
||||
|
||||
var RotateDrawable.isPivotXRelativeCompat: Boolean
|
||||
get() = isPivotXRelative
|
||||
set(value) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
isPivotXRelative = value
|
||||
} else {
|
||||
if (isPivotXRelative != value) {
|
||||
rotateStateMPivotXRelField.setBoolean(rotateDrawableMStateField.get(this), value)
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val rotateStateMPivotXField by lazyReflectedField(ROTATE_STATE_CLASS_NAME, "mPivotX")
|
||||
|
||||
var RotateDrawable.pivotXCompat: Float
|
||||
get() = pivotX
|
||||
set(value) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
pivotX = value
|
||||
} else {
|
||||
if (pivotX != value) {
|
||||
rotateStateMPivotXField.setFloat(rotateDrawableMStateField.get(this), value)
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val rotateStateMPivotYRelField by lazyReflectedField(ROTATE_STATE_CLASS_NAME, "mPivotYRel")
|
||||
|
||||
var RotateDrawable.isPivotYRelativeCompat: Boolean
|
||||
get() = isPivotYRelative
|
||||
set(value) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
isPivotYRelative = value
|
||||
} else {
|
||||
if (isPivotYRelative != value) {
|
||||
rotateStateMPivotYRelField.setBoolean(rotateDrawableMStateField.get(this), value)
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val rotateStateMPivotYField by lazyReflectedField(ROTATE_STATE_CLASS_NAME, "mPivotY")
|
||||
|
||||
var RotateDrawable.pivotYCompat: Float
|
||||
get() = pivotY
|
||||
set(value) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
pivotY = value
|
||||
} else {
|
||||
if (pivotY != value) {
|
||||
rotateStateMPivotYField.setFloat(rotateDrawableMStateField.get(this), value)
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import me.zhanghai.android.files.hiddenapi.RestrictedHiddenApi
|
||||
import me.zhanghai.android.files.util.lazyReflectedClass
|
||||
import me.zhanghai.android.files.util.lazyReflectedMethod
|
||||
import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
|
||||
/*
|
||||
* @see android.os.SELinux
|
||||
* @see <a href="https://android.googlesource.com/platform/frameworks/base/+/jb-mr1-release/core/java/android/os/SELinux.java">
|
||||
* jb-mr1-release/SELinux.java</a>
|
||||
* @see <a href="https://android.googlesource.com/platform/prebuilts/runtime/+/master/appcompat/hiddenapi-light-greylist.txt">
|
||||
* hiddenapi-light-greylist.txt</a>
|
||||
*/
|
||||
object SELinuxCompat {
|
||||
private val seLinuxClass by lazyReflectedClass("android.os.SELinux")
|
||||
private val isSELinuxEnabledMethod by lazyReflectedMethod(seLinuxClass, "isSELinuxEnabled")
|
||||
private val isSELinuxEnforcedMethod by lazyReflectedMethod(seLinuxClass, "isSELinuxEnforced")
|
||||
@RestrictedHiddenApi
|
||||
private val setFSCreateContextMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "setFSCreateContext", String::class.java
|
||||
)
|
||||
@RestrictedHiddenApi
|
||||
private val setFileContextMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "setFileContext", String::class.java, String::class.java
|
||||
)
|
||||
private val getFileContextStringMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "getFileContext", String::class.java
|
||||
)
|
||||
@RestrictedHiddenApi
|
||||
private val getPeerContextMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "getPeerContext", FileDescriptor::class.java
|
||||
)
|
||||
@get:RequiresApi(Build.VERSION_CODES.Q)
|
||||
@RestrictedHiddenApi
|
||||
private val getFileContextFileDescriptorMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "getFileContext", FileDescriptor::class.java
|
||||
)
|
||||
private val getContextMethod by lazyReflectedMethod(seLinuxClass, "getContext")
|
||||
private val getPidContextMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "getPidContext", Int::class.java
|
||||
)
|
||||
private val checkSELinuxAccessMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "checkSELinuxAccess", String::class.java, String::class.java,
|
||||
String::class.java, String::class.java
|
||||
)
|
||||
@RestrictedHiddenApi
|
||||
private val nativeRestoreconMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "native_restorecon", String::class.java, Int::class.java
|
||||
)
|
||||
@RestrictedHiddenApi
|
||||
private val restoreconStringMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "restorecon", String::class.java
|
||||
)
|
||||
@RestrictedHiddenApi
|
||||
private val restoreconFileMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "restorecon", File::class.java
|
||||
)
|
||||
private val restoreconRecursiveMethod by lazyReflectedMethod(
|
||||
seLinuxClass, "restoreconRecursive", File::class.java
|
||||
)
|
||||
|
||||
val isSELinuxEnabled: Boolean
|
||||
get() = isSELinuxEnabledMethod.invoke(null) as Boolean
|
||||
|
||||
val isSELinuxEnforced: Boolean
|
||||
get() = isSELinuxEnforcedMethod.invoke(null) as Boolean
|
||||
|
||||
fun setFSCreateContext(context: String?): Boolean =
|
||||
setFSCreateContextMethod.invoke(null, context) as Boolean
|
||||
|
||||
fun setFileContext(path: String, context: String): Boolean =
|
||||
setFileContextMethod.invoke(null, path, context) as Boolean
|
||||
|
||||
fun getFileContext(path: String): String? =
|
||||
getFileContextStringMethod.invoke(null, path) as String?
|
||||
|
||||
fun getPeerContext(fd: FileDescriptor): String? =
|
||||
getPeerContextMethod.invoke(null, fd) as String?
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
fun getFileContext(fd: FileDescriptor): String? =
|
||||
getFileContextFileDescriptorMethod.invoke(null, fd) as String?
|
||||
|
||||
val context: String?
|
||||
get() = getContextMethod.invoke(null) as String?
|
||||
|
||||
fun getPidContext(pid: Int): String? = getPidContextMethod.invoke(null, pid) as String?
|
||||
|
||||
fun checkSELinuxAccess(scon: String, tcon: String, tclass: String, perm: String): Boolean =
|
||||
checkSELinuxAccessMethod.invoke(null, scon, tcon, tclass, perm) as Boolean
|
||||
|
||||
fun native_restorecon(pathname: String?, flags: Int): Boolean =
|
||||
nativeRestoreconMethod.invoke(null, pathname, flags) as Boolean
|
||||
|
||||
fun restorecon(pathname: String): Boolean =
|
||||
restoreconStringMethod.invoke(null, pathname) as Boolean
|
||||
|
||||
fun restorecon(file: File): Boolean = restoreconFileMethod.invoke(null, file) as Boolean
|
||||
|
||||
fun restoreconRecursive(file: File): Boolean =
|
||||
restoreconRecursiveMethod.invoke(null, file) as Boolean
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import java8.nio.channels.SeekableByteChannel
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.SeekableByteChannel as JavaSeekableByteChannel
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
fun SeekableByteChannel.toJavaSeekableByteChannel(): JavaSeekableByteChannel =
|
||||
// java8.nio.channels.FileChannel extends from java.nio.channels.FileChannel, so in that case
|
||||
// the current object may already be implementing java.nio.channels.SeekableByteChannel.
|
||||
this as? JavaSeekableByteChannel ?: DelegateJavaSeekableByteChannel(this)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
private class DelegateJavaSeekableByteChannel(
|
||||
private val channel: SeekableByteChannel
|
||||
) : JavaSeekableByteChannel {
|
||||
@Throws(IOException::class)
|
||||
override fun read(dst: ByteBuffer): Int = channel.read(dst)
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(src: ByteBuffer): Int = channel.write(src)
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun position(): Long = channel.position()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun position(newPosition: Long): DelegateJavaSeekableByteChannel {
|
||||
channel.position(newPosition)
|
||||
return this
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun size(): Long = channel.size()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun truncate(size: Long): DelegateJavaSeekableByteChannel {
|
||||
channel.truncate(size)
|
||||
return this
|
||||
}
|
||||
|
||||
override fun isOpen(): Boolean = channel.isOpen
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
channel.close()
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* 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)
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.storage.StorageManager
|
||||
import android.os.storage.StorageVolume
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.zhanghai.android.files.util.lazyReflectedMethod
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
private val getVolumeListMethod by lazyReflectedMethod(StorageManager::class.java, "getVolumeList")
|
||||
|
||||
val StorageManager.storageVolumesCompat: List<StorageVolume>
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
storageVolumes
|
||||
} else {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(getVolumeListMethod.invoke(this) as Array<StorageVolume>).toList()
|
||||
}
|
||||
|
||||
// Thanks to fython for https://gist.github.com/fython/924f8d9019bca75d22de116bb69a54a1
|
||||
@Throws(IOException::class)
|
||||
fun StorageManager.openProxyFileDescriptorCompat(
|
||||
mode: Int,
|
||||
callback: ProxyFileDescriptorCallbackCompat,
|
||||
handler: Handler
|
||||
): ParcelFileDescriptor =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
openProxyFileDescriptor(mode, callback.toProxyFileDescriptorCallback(), handler)
|
||||
} else {
|
||||
// TODO: Support other modes?
|
||||
if (mode != ParcelFileDescriptor.MODE_READ_ONLY) {
|
||||
throw UnsupportedOperationException("mode $mode")
|
||||
}
|
||||
val pfds = ParcelFileDescriptor.createReliablePipe()
|
||||
PipeWriter(pfds[1], callback, handler).start()
|
||||
pfds[0]
|
||||
}
|
||||
|
||||
private class PipeWriter(
|
||||
private val pfd: ParcelFileDescriptor,
|
||||
private val callback: ProxyFileDescriptorCallbackCompat,
|
||||
private val handler: Handler
|
||||
) : Thread("StorageManagerCompat.PipeWriter-${id.getAndIncrement()}") {
|
||||
override fun run() {
|
||||
try {
|
||||
ParcelFileDescriptor.AutoCloseOutputStream(pfd).use { outputStream ->
|
||||
var offset = 0L
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
while (true) {
|
||||
val size = runBlocking {
|
||||
callback.awaitOnRead(offset, buffer.size, buffer, handler)
|
||||
}
|
||||
if (size == 0) {
|
||||
break
|
||||
}
|
||||
offset += size.toLong()
|
||||
outputStream.write(buffer, 0, size)
|
||||
}
|
||||
runBlocking { callback.awaitOnRelease(handler) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
try {
|
||||
pfd.closeWithError(e.message)
|
||||
} catch (e2: IOException) {
|
||||
e2.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val id = AtomicInteger()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ProxyFileDescriptorCallbackCompat.awaitOnRead(
|
||||
offset: Long,
|
||||
size: Int,
|
||||
data: ByteArray,
|
||||
handler: Handler
|
||||
): Int =
|
||||
suspendCoroutine { continuation ->
|
||||
handler.post {
|
||||
val readSize = try {
|
||||
onRead(offset, size, data)
|
||||
} catch (t: Throwable) {
|
||||
continuation.resumeWithException(t)
|
||||
return@post
|
||||
}
|
||||
continuation.resume(readSize)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ProxyFileDescriptorCallbackCompat.awaitOnRelease(handler: Handler) {
|
||||
suspendCoroutine<Unit> { continuation ->
|
||||
handler.post {
|
||||
try {
|
||||
onRelease()
|
||||
} catch (t: Throwable) {
|
||||
continuation.resumeWithException(t)
|
||||
return@post
|
||||
}
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2018 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageVolume
|
||||
import android.provider.DocumentsContract
|
||||
import me.zhanghai.android.files.util.lazyReflectedMethod
|
||||
import java.io.File
|
||||
|
||||
// Work around @SuppressLint not applicable to top level property with delegate.
|
||||
@SuppressLint("NewApi")
|
||||
private val storageVolumeClass = StorageVolume::class.java
|
||||
|
||||
private val getPathMethod by lazyReflectedMethod(storageVolumeClass, "getPath")
|
||||
|
||||
val StorageVolume.pathCompat: String
|
||||
get() = getPathMethod.invoke(this) as String
|
||||
|
||||
private val getPathFileMethod by lazyReflectedMethod(storageVolumeClass, "getPathFile")
|
||||
|
||||
val StorageVolume.pathFileCompat: File
|
||||
get() = File(pathCompat)
|
||||
|
||||
val StorageVolume.directoryCompat: File?
|
||||
get() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
directory
|
||||
} else {
|
||||
when (stateCompat) {
|
||||
Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY -> pathFileCompat
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context)
|
||||
|
||||
val StorageVolume.isPrimaryCompat: Boolean
|
||||
@SuppressLint("NewApi")
|
||||
get() = isPrimary
|
||||
|
||||
val StorageVolume.isRemovableCompat: Boolean
|
||||
@SuppressLint("NewApi")
|
||||
get() = isRemovable
|
||||
|
||||
val StorageVolume.isEmulatedCompat: Boolean
|
||||
@SuppressLint("NewApi")
|
||||
get() = isEmulated
|
||||
|
||||
val StorageVolume.uuidCompat: String?
|
||||
@SuppressLint("NewApi")
|
||||
get() = uuid
|
||||
|
||||
val StorageVolume.stateCompat: String
|
||||
@SuppressLint("NewApi")
|
||||
get() = state
|
||||
|
||||
fun StorageVolume.createOpenDocumentTreeIntentCompat(): Intent =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
createOpenDocumentTreeIntent()
|
||||
} else {
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||
val rootId = if (isEmulatedCompat) {
|
||||
DocumentsContractCompat.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID
|
||||
} else {
|
||||
uuidCompat
|
||||
}
|
||||
val rootUri = DocumentsContract.buildRootUri(
|
||||
DocumentsContractCompat.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, rootId
|
||||
)
|
||||
putExtra(DocumentsContractCompat.EXTRA_INITIAL_URI, rootUri)
|
||||
putExtra(DocumentsContractCompat.EXTRA_SHOW_ADVANCED, true)
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.os.Build
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun <T> KClass<ThreadLocal<*>>.withInitial(supplier: () -> T): ThreadLocal<T> =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ThreadLocal.withInitial(supplier)
|
||||
} else {
|
||||
object : ThreadLocal<T>() {
|
||||
override fun initialValue(): T = supplier.invoke()
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* 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)
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.view.ViewCompat
|
||||
import me.zhanghai.android.foregroundcompat.ForegroundCompat
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : View> View.requireViewByIdCompat(@IdRes id: Int): T =
|
||||
ViewCompat.requireViewById(this, id) as T
|
||||
|
||||
var View.scrollIndicatorsCompat: Int
|
||||
get() = ViewCompat.getScrollIndicators(this)
|
||||
set(value) {
|
||||
ViewCompat.setScrollIndicators(this, value)
|
||||
}
|
||||
|
||||
fun View.setScrollIndicatorsCompat(indicators: Int, mask: Int) {
|
||||
ViewCompat.setScrollIndicators(this, indicators, mask)
|
||||
}
|
||||
|
||||
var View.foregroundCompat: Drawable?
|
||||
get() = ForegroundCompat.getForeground(this)
|
||||
set(value) {
|
||||
ForegroundCompat.setForeground(this, value)
|
||||
}
|
||||
|
||||
var View.foregroundGravityCompat: Int
|
||||
get() = ForegroundCompat.getForegroundGravity(this)
|
||||
set(value) {
|
||||
ForegroundCompat.setForegroundGravity(this, value)
|
||||
}
|
||||
|
||||
var View.foregroundTintListCompat: ColorStateList?
|
||||
get() = ForegroundCompat.getForegroundTintList(this)
|
||||
set(value) {
|
||||
ForegroundCompat.setForegroundTintList(this, value)
|
||||
}
|
||||
|
||||
var View.foregroundTintModeCompat: PorterDuff.Mode?
|
||||
get() = ForegroundCompat.getForegroundTintMode(this)
|
||||
set(value) {
|
||||
ForegroundCompat.setForegroundTintMode(this, value)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user