├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── salt │ │ └── video │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── UPDATE.md │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ ├── dylanc │ │ │ └── activityresult │ │ │ │ └── launcher │ │ │ │ ├── ActivityResultCaller.kt │ │ │ │ ├── AppDetailsSettingLauncher.kt │ │ │ │ ├── BaseActivityResultLauncher.java │ │ │ │ ├── BaseActivityResultLauncher.kt │ │ │ │ ├── CreateDocumentLauncher.kt │ │ │ │ ├── CropPictureLauncher.kt │ │ │ │ ├── DeleteUriLauncher.kt │ │ │ │ ├── EnableBluetoothLauncher.kt │ │ │ │ ├── EnableLocationLauncher.kt │ │ │ │ ├── FileProviderUtils.kt │ │ │ │ ├── GetContentLauncher.kt │ │ │ │ ├── GetMultipleContentsLauncher.kt │ │ │ │ ├── OpenDocumentLauncher.kt │ │ │ │ ├── OpenDocumentTreeLauncher.kt │ │ │ │ ├── OpenMultipleDocumentsLauncher.kt │ │ │ │ ├── PickContactLauncher.kt │ │ │ │ ├── PickContentLauncher.kt │ │ │ │ ├── RequestMultiplePermissionsLauncher.kt │ │ │ │ ├── RequestPermissionLauncher.kt │ │ │ │ ├── SaveAsLauncher.kt │ │ │ │ ├── StartActivityLauncher.kt │ │ │ │ ├── StartIntentSenderLauncher.kt │ │ │ │ └── TakePicturePreviewLauncher.kt │ │ │ └── salt │ │ │ └── video │ │ │ ├── App.kt │ │ │ ├── core │ │ │ ├── PlayerState.kt │ │ │ └── SaltVideoPlayer.kt │ │ │ ├── data │ │ │ ├── AppDatabase.kt │ │ │ ├── dao │ │ │ │ ├── MediaSourceDao.kt │ │ │ │ └── VideoDao.kt │ │ │ ├── entry │ │ │ │ ├── MediaSource.kt │ │ │ │ └── Video.kt │ │ │ ├── ext │ │ │ │ └── VideoExt.kt │ │ │ ├── fui │ │ │ │ └── MediaSourceFui.kt │ │ │ └── repo │ │ │ │ └── MediaSourceRepo.kt │ │ │ ├── ui │ │ │ ├── about │ │ │ │ └── AboutActivity.kt │ │ │ ├── base │ │ │ │ ├── BasicActivityColumn.kt │ │ │ │ └── LazyFragment.kt │ │ │ ├── dialogx │ │ │ │ └── XSMStyle.kt │ │ │ ├── localfolder │ │ │ │ ├── LocalFolder.kt │ │ │ │ ├── LocalFolderActivity.kt │ │ │ │ └── LocalFolderViewModel.kt │ │ │ ├── main │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── my │ │ │ │ │ ├── MyFragment.kt │ │ │ │ │ ├── MyScreen.kt │ │ │ │ │ └── OpenSourceDialog.kt │ │ │ │ └── video │ │ │ │ │ ├── AddMediaSourceDialog.kt │ │ │ │ │ ├── QuickPlayDialog.kt │ │ │ │ │ ├── VideoFragment.kt │ │ │ │ │ ├── VideoScreen.kt │ │ │ │ │ └── VideoViewModel.kt │ │ │ ├── player │ │ │ │ ├── BottomBarUI.kt │ │ │ │ ├── PlayerActivity.kt │ │ │ │ ├── PlayerFloatUI.kt │ │ │ │ ├── PlayerPanel.kt │ │ │ │ ├── PlayerPanelState.kt │ │ │ │ ├── PlayerViewModel.kt │ │ │ │ └── TitleBarUI.kt │ │ │ ├── settings │ │ │ │ └── SettingsActivity.kt │ │ │ ├── theme │ │ │ │ └── VideoTheme.kt │ │ │ ├── user │ │ │ │ └── UserLoginActivity.kt │ │ │ ├── webdav │ │ │ │ └── AddWebDAVActivity.kt │ │ │ └── widget │ │ │ │ └── ProtectView.kt │ │ │ └── util │ │ │ ├── Config.kt │ │ │ ├── DocumentFileUtil.kt │ │ │ ├── StringExt.kt │ │ │ └── sort │ │ │ ├── AbstractSimpleNaturalComparator.java │ │ │ └── SimpleNaturalComparator.java │ └── res │ │ ├── drawable │ │ ├── background_seek_bar_music.xml │ │ ├── background_seek_bar_thumb.xml │ │ ├── bg_button.xml │ │ ├── bg_player_ui_bottom.xml │ │ ├── bg_player_ui_top.xml │ │ ├── cursor_xsm.xml │ │ ├── ic_about.xml │ │ ├── ic_add.xml │ │ ├── ic_arrow.png │ │ ├── ic_audio_file.xml │ │ ├── ic_avatar_test.jpg │ │ ├── ic_back.xml │ │ ├── ic_cloud.png │ │ ├── ic_copyright.xml │ │ ├── ic_cpu.png │ │ ├── ic_fast_forward.xml │ │ ├── ic_folder.xml │ │ ├── ic_foot.png │ │ ├── ic_forward.xml │ │ ├── ic_hide_file.png │ │ ├── ic_hide_source.xml │ │ ├── ic_hide_video.png │ │ ├── ic_home.xml │ │ ├── ic_internet.png │ │ ├── ic_kayaking.xml │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_license.png │ │ ├── ic_loading.png │ │ ├── ic_main_bottom_video.xml │ │ ├── ic_mini_player_pause.xml │ │ ├── ic_mini_player_play.xml │ │ ├── ic_movies_folder.png │ │ ├── ic_orange.xml │ │ ├── ic_pause.xml │ │ ├── ic_picture_in_picture.xml │ │ ├── ic_play.xml │ │ ├── ic_salt_video.xml │ │ ├── ic_screen_rotation.xml │ │ ├── ic_settings.xml │ │ ├── ic_snowman.png │ │ ├── ic_tv.png │ │ ├── ic_video.png │ │ ├── ic_video_file.xml │ │ ├── ic_wifi_tethering.xml │ │ └── seek_bar_thumb_btn.xml │ │ ├── layout │ │ ├── activity_local_folder.xml │ │ ├── activity_main.xml │ │ ├── activity_player.xml │ │ ├── fragment_my.xml │ │ ├── fragment_video.xml │ │ ├── layout_dialogx_xsm_dark.xml │ │ ├── layout_salt_video_player.xml │ │ ├── rv_home.xml │ │ ├── rv_home_footer.xml │ │ ├── rv_media_source.xml │ │ └── rv_video.xml │ │ ├── menu │ │ └── menu_main_bottom.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-v27 │ │ └── themes.xml │ │ ├── values-v29 │ │ └── themes.xml │ │ ├── values-zh │ │ └── strings.xml │ │ ├── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── com │ └── salt │ └── video │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 椒盐视频 SaltVideo 2 | 3 | 2024 年 3 月 14 日 GPL3 开源
4 | GPL3 open source on March 14, 2024 5 | 6 | 本地视频播放器,大体设计还是一个多 Activity,部分使用 Jetpack Compose ,播放器为 GSYVideoPlayer
7 | The local video player is generally designed as a multi-Activity, partially using Jetpack Compose, and the player is GSYVideoPlayer 8 | 9 | 软件不维护了,但如果有 PR 救活也是很开心的
10 | The software is no longer maintained, but I would be very happy if there is a PR to save it. 11 | 12 | 代码比较烂,随意看看咯
13 | The code is pretty bad, feel free to take a look 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | } 6 | 7 | def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.trim()) 8 | 9 | android { 10 | namespace 'com.salt.video' 11 | compileSdk 34 12 | 13 | defaultConfig { 14 | applicationId "com.salt.video" 15 | minSdk 24 16 | targetSdk 33 17 | versionCode gitCommits 18 | versionName "0.1.230726-build" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | 22 | ndk { 23 | abiFilters "arm64-v8a" 24 | } 25 | 26 | renderscriptTargetApi 21 27 | renderscriptSupportModeEnabled true 28 | } 29 | 30 | buildFeatures { 31 | viewBinding true 32 | compose true 33 | buildConfig true 34 | } 35 | 36 | composeOptions { 37 | kotlinCompilerExtensionVersion = "1.4.6" 38 | } 39 | 40 | buildTypes { 41 | debug { 42 | minifyEnabled false 43 | shrinkResources false 44 | applicationIdSuffix '.debug' 45 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 46 | } 47 | release { 48 | minifyEnabled true 49 | shrinkResources true 50 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 51 | } 52 | 53 | android.applicationVariants.all { variant -> 54 | variant.outputs.all { 55 | outputFileName = "SaltVideo-${defaultConfig.versionName}-${defaultConfig.versionCode}.apk" 56 | } 57 | } 58 | } 59 | 60 | compileOptions { 61 | sourceCompatibility JavaVersion.VERSION_17 62 | targetCompatibility JavaVersion.VERSION_17 63 | } 64 | 65 | kotlinOptions { 66 | jvmTarget = '17' 67 | } 68 | 69 | packagingOptions { 70 | exclude 'META-INF/DEPENDENCIES' 71 | exclude 'META-INF/LICENSE.md' 72 | exclude 'META-INF/NOTICE.md' 73 | } 74 | } 75 | 76 | dependencies { 77 | implementation 'androidx.core:core-ktx:1.9.0' 78 | implementation "androidx.activity:activity-ktx:1.6.1" 79 | implementation "androidx.fragment:fragment:1.5.5" 80 | implementation "androidx.fragment:fragment-ktx:1.5.5" 81 | implementation 'androidx.appcompat:appcompat:1.5.1' 82 | implementation 'com.google.android.material:material:1.7.0' 83 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 84 | testImplementation 'junit:junit:4.13.2' 85 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 86 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' 87 | 88 | // GSYVideoPlayer 89 | def gsy_video_player_version = "v8.3.4-release-jitpack" 90 | implementation "com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:$gsy_video_player_version" 91 | implementation "com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:$gsy_video_player_version" 92 | implementation "com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-arm64:$gsy_video_player_version" 93 | 94 | // implementation 'com.github.DylanCaiCoding:ActivityResultLauncher:1.1.2' 95 | implementation 'com.github.DylanCaiCoding:Callbacks:1.0.0' 96 | implementation('com.blankj:utilcodex:1.31.1') 97 | implementation 'com.github.Dimezis:BlurView:version-2.0.3' 98 | 99 | def dialogx_version = "0.0.47.beta19" 100 | implementation "com.github.kongzue.DialogX:DialogX:${dialogx_version}" 101 | implementation "com.github.kongzue.DialogX:DialogXMIUIStyle:${dialogx_version}" 102 | implementation "com.github.kongzue.DialogX:DialogXMaterialYou:${dialogx_version}" 103 | implementation "com.github.kongzue.DialogX:DialogXIOSStyle:${dialogx_version}" 104 | implementation "com.github.kongzue.DialogX:DialogXKongzueStyle:${dialogx_version}" 105 | 106 | implementation("com.github.team403:DsoKotlinExtensions:1.0.3") 107 | implementation 'com.github.ibrahimsn98:SmoothBottomBar:1.7.8' 108 | implementation("androidx.recyclerview:recyclerview:1.2.1") 109 | implementation 'com.github.liangjingkanji:BRV:1.3.82' 110 | 111 | def room_version = "2.5.2" 112 | implementation "androidx.room:room-runtime:$room_version" 113 | implementation "androidx.room:room-ktx:$room_version" 114 | annotationProcessor "androidx.room:room-compiler:$room_version" 115 | kapt("androidx.room:room-compiler:$room_version") 116 | kapt "android.arch.persistence.room:compiler:1.1.1" 117 | 118 | def lifecycle_version = "2.5.1" 119 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 120 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 121 | 122 | implementation("io.coil-kt:coil:2.2.2") 123 | implementation("io.coil-kt:coil-video:2.2.2") 124 | 125 | // landscapist 图片加载 126 | def landscapist_version = "2.1.11" 127 | implementation "com.github.skydoves:landscapist-glide:$landscapist_version" 128 | 129 | def glide_version = "4.15.1" 130 | implementation "com.github.bumptech.glide:glide:$glide_version" 131 | kapt "com.github.bumptech.glide:compiler:$glide_version" 132 | 133 | def composeBom = platform('androidx.compose:compose-bom:2023.05.01') 134 | implementation composeBom 135 | androidTestImplementation composeBom 136 | // or Material Design 2 137 | implementation 'androidx.compose.material:material' 138 | // or skip Material Design and build directly on top of foundational components 139 | implementation 'androidx.compose.foundation:foundation' 140 | // or only import the main APIs for the underlying toolkit systems, 141 | // such as input and measurement/layout 142 | implementation 'androidx.compose.ui:ui' 143 | implementation 'androidx.activity:activity-compose:1.6.1' 144 | implementation("com.github.Moriafly:SaltUI:0.1.0-dev14") 145 | 146 | implementation("com.tencent:mmkv-static:1.2.13") 147 | implementation 'com.github.1552980358:C2Pinyin:2.3.3' 148 | // WebDAV 149 | // implementation("com.github.thegrizzlylabs:sardine-android:0.8") 150 | 151 | implementation 'com.github.lookfirst:sardine:5.10' 152 | 153 | implementation 'com.squareup.okhttp3:okhttp:4.10.0' 154 | implementation 'com.github.liangjingkanji:Net:3.5.9' 155 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | # 一般 5 就行 24 | -optimizationpasses 10 25 | -dontusemixedcaseclassnames 26 | -dontskipnonpubliclibraryclasses 27 | -dontpreverify 28 | -verbose 29 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 30 | -keep public class * extends android.app.Activity 31 | -keep public class * extends android.app.Application 32 | -keep public class * extends android.app.Service 33 | -keep public class * extends android.content.BroadcastReceiver 34 | -keep public class * extends android.content.Intent 35 | -keep public class * extends android.content.ContextWrapper 36 | -keep public class * extends android.content.ContentProvider 37 | -keep public class * extends android.app.backup.BackupAgentHelper 38 | -keep public class * extends android.preference.Preference 39 | 40 | # GSYVideoPlayer 41 | -keep class com.shuyu.gsyvideoplayer.video.** { *; } 42 | -dontwarn com.shuyu.gsyvideoplayer.video.** 43 | -keep class com.shuyu.gsyvideoplayer.video.base.** { *; } 44 | -dontwarn com.shuyu.gsyvideoplayer.video.base.** 45 | -keep class com.shuyu.gsyvideoplayer.utils.** { *; } 46 | -dontwarn com.shuyu.gsyvideoplayer.utils.** 47 | -keep class tv.danmaku.ijk.** { *; } 48 | -dontwarn tv.danmaku.ijk.** 49 | -keep class com.google.android.exoplayer2.** {*;} 50 | -keep interface com.google.android.exoplayer2.** 51 | 52 | -keep public class * extends android.view.View{ 53 | *** get*(); 54 | void set*(***); 55 | public (android.content.Context); 56 | public (android.content.Context, java.lang.Boolean); 57 | public (android.content.Context, android.util.AttributeSet); 58 | public (android.content.Context, android.util.AttributeSet, int); 59 | } 60 | 61 | -dontwarn org.bouncycastle.jsse.** 62 | -dontwarn org.conscrypt.** 63 | -dontwarn org.openjsse.** 64 | 65 | # 将所有混淆的类移动到 androidx.core 66 | -repackageclasses androidx.core -------------------------------------------------------------------------------- /app/src/androidTest/java/com/salt/video/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.salt.video", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 16 | 19 | 22 | 25 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/assets/UPDATE.md: -------------------------------------------------------------------------------- 1 | 0.1.230726 2 | 我的界面优化 3 | 支持多种倍数调节 4 | 长按显示 X2 倍数框 5 | 其他改进 6 | 7 | 0.4.0 20220119 8 | 新增长按 2x 倍数 9 | UI 调整 10 | 11 | 0.3.0 20220104 12 | 【本地】和【云端】合并为【视频】 13 | 退出播放界面新增提示框 14 | 修复关闭画中画声音不停止的问题 15 | 修复深色模式下出现标题栏的问题 16 | 最低支持 Android 7 17 | 18 | 0.2.1 20221223 19 | 画中画适配视频比例 20 | 适配 HDR10+ 21 | 22 | 0.2.0 20221221 23 | 确定软件主页设计框架 24 | 25 | 0.1.3 20221220 26 | 拖动播放进度可以实时进度条响应 27 | 支持文件管理器等调用播放 28 | 29 | 0.1.2 20221220 30 | 新的首页 31 | 新增播放本地音乐功能 32 | 新增画中画功能 33 | 34 | 0.1.1 20221219 35 | 新增播放进度、视频长度指示 36 | 初始化播放界面可以自动根据视频大小调整屏幕旋转 37 | 优化播放进度条透明度,加了一个手柄 -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Moriafly/SaltVideo/6c7295b22070ef010fef0308dbf88078bf038821/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/ActivityResultCaller.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.dylanc.activityresult.launcher 18 | 19 | import android.app.Activity 20 | import androidx.activity.result.ActivityResultCaller 21 | import androidx.core.app.ActivityCompat 22 | import androidx.fragment.app.Fragment 23 | 24 | /** 25 | * @author Dylan Cai 26 | */ 27 | 28 | internal fun ActivityResultCaller.shouldShowRequestPermissionRationale(permission: String) = 29 | when (this) { 30 | is Activity -> ActivityCompat.shouldShowRequestPermissionRationale(this, permission) 31 | is Fragment -> ActivityCompat.shouldShowRequestPermissionRationale(requireActivity(), permission) 32 | else -> false 33 | } 34 | 35 | val ActivityResultCaller.context 36 | get() = when (this) { 37 | is Activity -> this 38 | is Fragment -> this.requireContext() 39 | else -> throw IllegalArgumentException("The constructor's ActivityResultCaller argument must be Activity or Fragment.") 40 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/AppDetailsSettingLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.dylanc.activityresult.launcher 18 | 19 | import android.content.Context 20 | import android.content.Intent 21 | import android.net.Uri 22 | import android.provider.Settings 23 | import androidx.activity.result.ActivityResultCallback 24 | import androidx.activity.result.ActivityResultCaller 25 | import androidx.activity.result.contract.ActivityResultContract 26 | 27 | /** 28 | * @author Dylan Cai 29 | */ 30 | class AppDetailsSettingsLauncher(caller: ActivityResultCaller) : 31 | BaseActivityResultLauncher(caller, AppDetailsSettingsContract()) { 32 | 33 | @JvmOverloads 34 | fun launch(callback: ActivityResultCallback = ActivityResultCallback {}) = 35 | launch(null, callback) 36 | 37 | suspend fun launchForResult() = launchForResult(null) 38 | 39 | fun launchForFlow() = launchForFlow(null) 40 | } 41 | 42 | class AppDetailsSettingsContract : ActivityResultContract() { 43 | override fun createIntent(context: Context, input: Unit) = 44 | Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { 45 | data = Uri.fromParts("package", context.packageName, null) 46 | } 47 | 48 | override fun parseResult(resultCode: Int, intent: Intent?) {} 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/BaseActivityResultLauncher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.dylanc.activityresult.launcher; 18 | 19 | import android.annotation.SuppressLint; 20 | import android.content.Context; 21 | 22 | import androidx.activity.result.ActivityResultCallback; 23 | import androidx.activity.result.ActivityResultCaller; 24 | import androidx.activity.result.contract.ActivityResultContract; 25 | import androidx.annotation.NonNull; 26 | import androidx.annotation.Nullable; 27 | import androidx.core.app.ActivityOptionsCompat; 28 | import androidx.lifecycle.LiveData; 29 | import androidx.lifecycle.MutableLiveData; 30 | 31 | import com.dylanc.activityresult.launcher.ActivityResultCallerKt; 32 | 33 | /** 34 | * @author Dylan Cai 35 | */ 36 | @SuppressWarnings("unused") 37 | public class BaseActivityResultLauncher { 38 | 39 | private final androidx.activity.result.ActivityResultLauncher launcher; 40 | private final ActivityResultCaller caller; 41 | private ActivityResultCallback callback; 42 | private MutableLiveData unprocessedResult; 43 | 44 | public BaseActivityResultLauncher(@NonNull ActivityResultCaller caller, @NonNull ActivityResultContract contract) { 45 | this.caller = caller; 46 | launcher = caller.registerForActivityResult(contract, (result) -> { 47 | if (callback != null) { 48 | callback.onActivityResult(result); 49 | callback = null; 50 | } else { 51 | getUnprocessedResultLiveData().setValue(result); 52 | } 53 | }); 54 | } 55 | 56 | public void launch(@SuppressLint("UnknownNullness") I input, @NonNull ActivityResultCallback callback) { 57 | launch(input, null, callback); 58 | } 59 | 60 | public void launch(@SuppressLint("UnknownNullness") I input, @Nullable ActivityOptionsCompat options, 61 | @NonNull ActivityResultCallback callback) { 62 | this.callback = callback; 63 | launcher.launch(input, options); 64 | } 65 | 66 | public LiveData getUnprocessedResult() { 67 | return getUnprocessedResultLiveData(); 68 | } 69 | 70 | public Context getContext() { 71 | return ActivityResultCallerKt.getContext(caller); 72 | } 73 | 74 | private MutableLiveData getUnprocessedResultLiveData() { 75 | if (unprocessedResult == null) { 76 | unprocessedResult = new MutableLiveData<>(); 77 | } 78 | return unprocessedResult; 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/BaseActivityResultLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import kotlinx.coroutines.flow.flow 22 | import kotlinx.coroutines.suspendCancellableCoroutine 23 | import kotlin.coroutines.resume 24 | import kotlin.coroutines.suspendCoroutine 25 | 26 | /** 27 | * @author Dylan Cai 28 | */ 29 | 30 | suspend fun BaseActivityResultLauncher.launchForResult(input: I?) = 31 | suspendCancellableCoroutine { continuation -> 32 | launch(input) { 33 | if (it != null) { 34 | continuation.resume(it) 35 | } else { 36 | continuation.cancel() 37 | } 38 | } 39 | } 40 | 41 | suspend fun BaseActivityResultLauncher>.launchForNonEmptyResult(input: I?) = 42 | suspendCancellableCoroutine> { continuation -> 43 | launch(input) { 44 | if (it != null && it.isNotEmpty()) { 45 | continuation.resume(it) 46 | } else { 47 | continuation.cancel() 48 | } 49 | } 50 | } 51 | 52 | suspend fun BaseActivityResultLauncher.launchForNullableResult(input: I?) = 53 | suspendCoroutine { continuation -> 54 | launch(input) { continuation.resume(it) } 55 | } 56 | 57 | fun BaseActivityResultLauncher.launchForFlow(input: I?) = 58 | flow { emit(launchForResult(input)) } 59 | 60 | fun BaseActivityResultLauncher>.launchForNonEmptyFlow(input: I?) = 61 | flow { emit(launchForNonEmptyResult(input)) } 62 | 63 | fun BaseActivityResultLauncher.launchForNullableFlow(input: I?) = 64 | flow { emit(launchForNullableResult(input)) } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/CreateDocumentLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.net.Uri 22 | import androidx.activity.result.ActivityResultCallback 23 | import androidx.activity.result.ActivityResultCaller 24 | import androidx.activity.result.contract.ActivityResultContracts.CreateDocument 25 | 26 | /** 27 | * @author Dylan Cai 28 | */ 29 | class CreateDocumentLauncher(caller: ActivityResultCaller) : 30 | BaseActivityResultLauncher(caller, CreateDocument()) { 31 | 32 | fun launch(callback: ActivityResultCallback) = launch(null, callback) 33 | 34 | suspend fun launchForResult() = launchForResult(null) 35 | 36 | fun launchForFlow() = launchForFlow(null) 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/CropPictureLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.app.Activity 22 | import android.content.ContentValues 23 | import android.content.Context 24 | import android.content.Intent 25 | import android.graphics.Bitmap 26 | import android.net.Uri 27 | import android.os.Build 28 | import android.provider.MediaStore 29 | import androidx.activity.result.ActivityResultCallback 30 | import androidx.activity.result.ActivityResultCaller 31 | import androidx.activity.result.contract.ActivityResultContract 32 | import androidx.annotation.CallSuper 33 | import com.dylanc.callbacks.Callback1 34 | 35 | /** 36 | * @author Dylan Cai 37 | */ 38 | data class CropPictureRequest @JvmOverloads constructor( 39 | val inputUri: Uri, 40 | var aspectX: Int = 1, 41 | var aspectY: Int = 1, 42 | var outputX: Int = 512, 43 | var outputY: Int = 512, 44 | var outputContentValues: ContentValues = ContentValues(), 45 | var onCreateIntent: Callback1? = null 46 | ) 47 | 48 | class CropPictureLauncher(caller: ActivityResultCaller) : 49 | BaseActivityResultLauncher(caller, CropPictureContract()) { 50 | 51 | @JvmOverloads 52 | fun launch( 53 | inputUri: Uri, 54 | aspectX: Int = 1, aspectY: Int = 1, 55 | outputX: Int = 512, outputY: Int = 512, 56 | outputContentValues: ContentValues = ContentValues(), 57 | onCreateIntent: Callback1? = null, 58 | onActivityResult: ActivityResultCallback 59 | ) { 60 | val request = CropPictureRequest( 61 | inputUri, aspectX, aspectY, outputX, 62 | outputY, outputContentValues, onCreateIntent 63 | ) 64 | launch(request, onActivityResult) 65 | } 66 | 67 | suspend fun launchForResult( 68 | inputUri: Uri, 69 | aspectX: Int = 1, aspectY: Int = 1, 70 | outputX: Int = 512, outputY: Int = 512, 71 | outputContentValues: ContentValues = ContentValues(), 72 | onCreateIntent: Callback1? = null 73 | ) = 74 | CropPictureRequest( 75 | inputUri, aspectX, aspectY, outputX, 76 | outputY, outputContentValues, onCreateIntent 77 | ).let { launchForResult(it) } 78 | } 79 | 80 | class CropPictureContract : ActivityResultContract() { 81 | private lateinit var outputUri: Uri 82 | 83 | @CallSuper 84 | override fun createIntent(context: Context, input: CropPictureRequest) = 85 | Intent("com.android.camera.action.CROP").apply { 86 | outputUri = context.contentResolver.insert( 87 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 88 | input.outputContentValues 89 | )!! 90 | setDataAndType(input.inputUri, "image/*") 91 | putExtra(MediaStore.EXTRA_OUTPUT, outputUri) 92 | putExtra("aspectX", input.aspectX) 93 | putExtra("aspectY", input.aspectY) 94 | putExtra("outputX", input.outputX) 95 | putExtra("outputY", input.outputY) 96 | putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()) 97 | putExtra("return-data", false) 98 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 99 | addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 100 | } 101 | input.onCreateIntent?.invoke(this) 102 | } 103 | 104 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? = 105 | if (resultCode == Activity.RESULT_OK) outputUri else null 106 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/DeleteUriLauncher.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.activityresult.launcher 2 | 3 | import android.app.Activity 4 | import android.app.RecoverableSecurityException 5 | import android.content.ContentUris 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.provider.MediaStore 9 | 10 | /** 11 | * @author Dylan Cai 12 | */ 13 | class DeleteUriLauncher { 14 | 15 | 16 | inline fun Activity.delete(contentUri: Uri, id: Long = ContentUris.parseId(contentUri)) { 17 | val requestCode = 100 18 | when { 19 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { 20 | val pendingIntent = MediaStore.createDeleteRequest(contentResolver, listOf(contentUri)) 21 | startIntentSenderForResult( 22 | pendingIntent.intentSender, requestCode, null, 23 | 0, 0, 0, null 24 | ) 25 | } 26 | 27 | Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> { 28 | try { 29 | val selection = "${MediaStore.MediaColumns._ID} = ?" 30 | val selectionArgs = arrayOf(id.toString()) 31 | contentResolver.delete(contentUri, selection, selectionArgs) 32 | } catch (ex: RecoverableSecurityException) { 33 | val intent = ex.userAction.actionIntent.intentSender 34 | startIntentSenderForResult( 35 | intent, requestCode, null, 36 | 0, 0, 0, null 37 | ) 38 | } 39 | } 40 | 41 | else -> { 42 | val selection = "${MediaStore.MediaColumns._ID} = ?" 43 | val selectionArgs = arrayOf(id.toString()) 44 | contentResolver.delete(contentUri, selection, selectionArgs) 45 | } 46 | } 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/EnableBluetoothLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.Manifest 22 | import android.app.Activity 23 | import android.bluetooth.BluetoothAdapter 24 | import android.content.Context 25 | import android.content.Intent 26 | import android.widget.Toast 27 | import androidx.activity.result.ActivityResultCallback 28 | import androidx.activity.result.ActivityResultCaller 29 | import androidx.activity.result.contract.ActivityResultContract 30 | import androidx.annotation.RequiresPermission 31 | import androidx.annotation.StringRes 32 | import com.dylanc.callbacks.Callback0 33 | import com.dylanc.callbacks.Callback1 34 | 35 | /** 36 | * @author Dylan Cai 37 | */ 38 | class EnableBluetoothLauncher(caller: ActivityResultCaller) : 39 | BaseActivityResultLauncher(caller, EnableBluetoothContract()) { 40 | 41 | private val requestPermissionLauncher = RequestPermissionLauncher(caller) 42 | private val enableLocationLauncher = EnableLocationLauncher(caller) 43 | 44 | @RequiresPermission(Manifest.permission.BLUETOOTH) 45 | fun launch(callback: ActivityResultCallback) = launch(null, callback) 46 | 47 | @RequiresPermission(Manifest.permission.BLUETOOTH) 48 | suspend fun launchForResult() = launchForResult(null) 49 | 50 | @RequiresPermission(Manifest.permission.BLUETOOTH) 51 | fun launchForFlow() = launchForFlow(null) 52 | 53 | @RequiresPermission(Manifest.permission.BLUETOOTH) 54 | override fun launch(input: Unit?, callback: ActivityResultCallback) { 55 | if (BluetoothAdapter.getDefaultAdapter()?.isEnabled != true) { 56 | super.launch(input, callback) 57 | } else { 58 | callback.onActivityResult(true) 59 | } 60 | } 61 | 62 | @JvmOverloads 63 | @RequiresPermission(Manifest.permission.BLUETOOTH) 64 | fun launch( 65 | onBluetoothEnabled: ActivityResultCallback, 66 | onPermissionDenied: Callback1, 67 | onExplainRequestPermission: Callback0? = null 68 | ) { 69 | launch { 70 | if (it) { 71 | requestPermissionLauncher.launch( 72 | Manifest.permission.ACCESS_FINE_LOCATION, 73 | onGranted = { onBluetoothEnabled.onActivityResult(true) }, 74 | onDenied = onPermissionDenied, 75 | onExplainRequest = onExplainRequestPermission 76 | ) 77 | } else { 78 | onBluetoothEnabled.onActivityResult(false) 79 | } 80 | } 81 | } 82 | 83 | @JvmOverloads 84 | @RequiresPermission(Manifest.permission.BLUETOOTH) 85 | fun launchAndEnableLocation( 86 | @StringRes enablePositionReason: Int, 87 | onLocationEnabled: ActivityResultCallback, 88 | onPermissionDenied: Callback1, 89 | onExplainRequestPermission: Callback0? = null, 90 | onBluetoothDisabled: Callback0? = null 91 | ) = 92 | launchAndEnableLocation( 93 | context.getString(enablePositionReason), onLocationEnabled, onPermissionDenied, 94 | onExplainRequestPermission, onBluetoothDisabled 95 | ) 96 | 97 | @JvmOverloads 98 | @RequiresPermission(Manifest.permission.BLUETOOTH) 99 | fun launchAndEnableLocation( 100 | enablePositionReason: String, 101 | onLocationEnabled: ActivityResultCallback, 102 | onPermissionDenied: Callback1, 103 | onExplainRequestPermission: Callback0? = null, 104 | onBluetoothDisabled: Callback0? = null 105 | ) = 106 | launchAndEnableLocation( 107 | { onLocationEnabled.onActivityResult(true) }, 108 | onPermissionDenied, onExplainRequestPermission, onBluetoothDisabled, 109 | { 110 | Toast.makeText(context, enablePositionReason, Toast.LENGTH_SHORT).show() 111 | it.launch(onLocationEnabled) 112 | }) 113 | 114 | @JvmOverloads 115 | @RequiresPermission(Manifest.permission.BLUETOOTH) 116 | fun launchAndEnableLocation( 117 | onLocationEnabled: Callback0, 118 | onPermissionDenied: Callback1, 119 | onExplainRequestPermission: Callback0? = null, 120 | onBluetoothDisabled: Callback0? = null, 121 | onLocationDisabled: Callback1 122 | ) { 123 | launch({ 124 | if (it && !context.isLocationEnabled) { 125 | onLocationDisabled(enableLocationLauncher) 126 | } else if (it) { 127 | onLocationEnabled() 128 | } else { 129 | onBluetoothDisabled?.invoke() 130 | } 131 | }, onPermissionDenied, onExplainRequestPermission) 132 | } 133 | } 134 | 135 | class EnableBluetoothContract : ActivityResultContract() { 136 | 137 | override fun createIntent(context: Context, input: Unit) = 138 | Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) 139 | 140 | override fun parseResult(resultCode: Int, intent: Intent?) = 141 | resultCode == Activity.RESULT_OK 142 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/EnableLocationLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.content.Context 22 | import android.content.Intent 23 | import android.location.LocationManager 24 | import android.provider.Settings 25 | import androidx.activity.result.ActivityResultCallback 26 | import androidx.activity.result.ActivityResultCaller 27 | import androidx.activity.result.contract.ActivityResultContract 28 | 29 | /** 30 | * @author Dylan Cai 31 | */ 32 | 33 | val Context.isLocationEnabled: Boolean 34 | get() = (getSystemService(Context.LOCATION_SERVICE) as LocationManager) 35 | .isProviderEnabled(LocationManager.GPS_PROVIDER) 36 | 37 | class EnableLocationLauncher(caller: ActivityResultCaller) : 38 | BaseActivityResultLauncher(caller, EnableLocationContract(caller)) { 39 | 40 | fun launch(callback: ActivityResultCallback) = launch(null, callback) 41 | 42 | suspend fun launchForResult() = launchForResult(null) 43 | 44 | fun launchForFlow() = launchForFlow(null) 45 | 46 | override fun launch(input: Unit?, callback: ActivityResultCallback) { 47 | if (!context.isLocationEnabled) { 48 | super.launch(input, callback) 49 | } else { 50 | callback.onActivityResult(true) 51 | } 52 | } 53 | } 54 | 55 | class EnableLocationContract(private val caller: ActivityResultCaller) : 56 | ActivityResultContract() { 57 | 58 | override fun createIntent(context: Context, input: Unit) = 59 | Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) 60 | 61 | override fun parseResult(resultCode: Int, intent: Intent?) = 62 | caller.context.isLocationEnabled 63 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/FileProviderUtils.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.activityresult.launcher 2 | 3 | /** 4 | * @author Dylan Cai 5 | */ 6 | object FileProviderUtils { 7 | var authority: String? = null 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/GetContentLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.Manifest 22 | import android.net.Uri 23 | import androidx.activity.result.ActivityResultCallback 24 | import androidx.activity.result.ActivityResultCaller 25 | import androidx.activity.result.contract.ActivityResultContracts.GetContent 26 | import com.dylanc.callbacks.Callback0 27 | import com.dylanc.callbacks.Callback1 28 | 29 | /** 30 | * @author Dylan Cai 31 | */ 32 | class GetContentLauncher(caller: ActivityResultCaller) : 33 | BaseActivityResultLauncher(caller, GetContent()) { 34 | 35 | private val requestPermissionLauncher = RequestPermissionLauncher(caller) 36 | 37 | suspend fun launchForImageResult() = launchForResult("image/*") 38 | 39 | suspend fun launchForVideoResult() = launchForResult("video/*") 40 | 41 | fun launchForImageFlow() = launchForFlow("image/*") 42 | 43 | fun launchForVideoFlow() = launchForFlow("video/*") 44 | 45 | @JvmOverloads 46 | fun launch( 47 | input: String, 48 | onActivityResult: ActivityResultCallback, 49 | onPermissionDenied: Callback1, 50 | onExplainRequestPermission: Callback0? = null 51 | ) { 52 | requestPermissionLauncher.launch( 53 | Manifest.permission.READ_EXTERNAL_STORAGE, 54 | onGranted = { launch(input, onActivityResult) }, 55 | onPermissionDenied, 56 | onExplainRequestPermission 57 | ) 58 | } 59 | 60 | @JvmOverloads 61 | fun launchForImage( 62 | onActivityResult: ActivityResultCallback, 63 | onPermissionDenied: Callback1, 64 | onExplainRequestPermission: Callback0? = null 65 | ) = launch("image/*", onActivityResult, onPermissionDenied, onExplainRequestPermission) 66 | 67 | @JvmOverloads 68 | fun launchForVideo( 69 | onActivityResult: ActivityResultCallback, 70 | onPermissionDenied: Callback1, 71 | onExplainRequestPermission: Callback0? = null 72 | ) = launch("video/*", onActivityResult, onPermissionDenied, onExplainRequestPermission) 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/GetMultipleContentsLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.Manifest 22 | import android.net.Uri 23 | import androidx.activity.result.ActivityResultCallback 24 | import androidx.activity.result.ActivityResultCaller 25 | import androidx.activity.result.contract.ActivityResultContracts.GetMultipleContents 26 | import com.dylanc.callbacks.Callback0 27 | import com.dylanc.callbacks.Callback1 28 | 29 | /** 30 | * @author Dylan Cai 31 | */ 32 | class GetMultipleContentsLauncher(caller: ActivityResultCaller) : 33 | BaseActivityResultLauncher>(caller, GetMultipleContents()) { 34 | 35 | private val permissionLauncher = RequestPermissionLauncher(caller) 36 | 37 | suspend fun launchForImageResult() = launchForNonEmptyResult("image/*") 38 | 39 | suspend fun launchForVideoResult() = launchForNonEmptyResult("video/*") 40 | 41 | fun launchForImageFlow() = launchForNonEmptyFlow("image/*") 42 | 43 | fun launchForVideoFlow() = launchForNonEmptyFlow("video/*") 44 | 45 | @JvmOverloads 46 | fun launch( 47 | input: String, 48 | onActivityResult: ActivityResultCallback>, 49 | onPermissionDenied: Callback1, 50 | onExplainRequestPermission: Callback0? = null 51 | ) { 52 | permissionLauncher.launch( 53 | Manifest.permission.READ_EXTERNAL_STORAGE, 54 | onGranted = { launch(input, onActivityResult) }, 55 | onPermissionDenied, 56 | onExplainRequestPermission 57 | ) 58 | } 59 | 60 | @JvmOverloads 61 | fun launchForImage( 62 | onActivityResult: ActivityResultCallback>, 63 | onPermissionDenied: Callback1, 64 | onExplainRequestPermission: Callback0? = null 65 | ) = launch("image/*", onActivityResult, onPermissionDenied, onExplainRequestPermission) 66 | 67 | @JvmOverloads 68 | fun launchForVideo( 69 | onActivityResult: ActivityResultCallback>, 70 | onPermissionDenied: Callback1, 71 | onExplainRequestPermission: Callback0? = null 72 | ) = launch("video/*", onActivityResult, onPermissionDenied, onExplainRequestPermission) 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/OpenDocumentLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.net.Uri 22 | import androidx.activity.result.ActivityResultCallback 23 | import androidx.activity.result.ActivityResultCaller 24 | import androidx.activity.result.contract.ActivityResultContracts.OpenDocument 25 | 26 | /** 27 | * @author Dylan Cai 28 | */ 29 | class OpenDocumentLauncher(caller: ActivityResultCaller) : BaseActivityResultLauncher, Uri>(caller, OpenDocument()) { 30 | 31 | fun launch(vararg input: String, callback: ActivityResultCallback) = 32 | launch(arrayOf(*input), callback) 33 | 34 | suspend fun launchForResult(vararg input: String) = launchForResult(arrayOf(*input)) 35 | 36 | fun launchForFlow(vararg input: String) = launchForFlow(arrayOf(*input)) 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/OpenDocumentTreeLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.net.Uri 22 | import androidx.activity.result.ActivityResultCallback 23 | import androidx.activity.result.ActivityResultCaller 24 | import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree 25 | 26 | /** 27 | * @author Dylan Cai 28 | */ 29 | class OpenDocumentTreeLauncher(caller: ActivityResultCaller) : BaseActivityResultLauncher(caller, OpenDocumentTree()) { 30 | 31 | fun launch(callback: ActivityResultCallback) = launch(null, callback) 32 | 33 | suspend fun launchForResult() = launchForResult(null) 34 | 35 | fun launchForFlow() = launchForFlow(null) 36 | 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/OpenMultipleDocumentsLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.net.Uri 22 | import androidx.activity.result.ActivityResultCallback 23 | import androidx.activity.result.ActivityResultCaller 24 | import androidx.activity.result.contract.ActivityResultContracts.OpenMultipleDocuments 25 | 26 | /** 27 | * @author Dylan Cai 28 | */ 29 | class OpenMultipleDocumentsLauncher(caller: ActivityResultCaller) : 30 | BaseActivityResultLauncher, List>(caller, OpenMultipleDocuments()) { 31 | 32 | @JvmName("launch2") 33 | fun launch(vararg input: String, callback: ActivityResultCallback>) = 34 | launch(arrayOf(*input), callback) 35 | 36 | suspend fun launchForNonEmptyResult(vararg input: String) = 37 | launchForNonEmptyResult(arrayOf(*input)) 38 | 39 | fun launchForNonEmptyFlow(vararg input: String) = 40 | launchForNonEmptyFlow(arrayOf(*input)) 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/PickContactLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.net.Uri 22 | import androidx.activity.result.ActivityResultCallback 23 | import androidx.activity.result.ActivityResultCaller 24 | import androidx.activity.result.contract.ActivityResultContracts.PickContact 25 | 26 | /** 27 | * @author Dylan Cai 28 | */ 29 | class PickContactLauncher(caller: ActivityResultCaller) : 30 | BaseActivityResultLauncher(caller, PickContact()) { 31 | 32 | fun launch(onActivityResult: ActivityResultCallback) = launch(null, onActivityResult) 33 | 34 | suspend fun launchForResult() = launchForResult(null) 35 | 36 | fun launchForFlow() = launchForFlow(null) 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/PickContentLauncher.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.dylanc.activityresult.launcher 4 | 5 | import android.Manifest 6 | import android.app.Activity 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.net.Uri 10 | import androidx.activity.result.ActivityResultCallback 11 | import androidx.activity.result.ActivityResultCaller 12 | import androidx.activity.result.contract.ActivityResultContract 13 | import com.dylanc.callbacks.Callback0 14 | import com.dylanc.callbacks.Callback1 15 | 16 | /** 17 | * @author Dylan Cai 18 | */ 19 | class PickContentLauncher(caller: ActivityResultCaller) : 20 | BaseActivityResultLauncher(caller, PickContentContract()) { 21 | 22 | private val requestPermissionLauncher = RequestPermissionLauncher(caller) 23 | 24 | suspend fun launchForImageResult() = launchForResult("image/*") 25 | 26 | suspend fun launchForVideoResult() = launchForResult("video/*") 27 | 28 | fun launchForImageFlow() = launchForFlow("image/*") 29 | 30 | fun launchForVideoFlow() = launchForFlow("video/*") 31 | 32 | @JvmOverloads 33 | fun launch( 34 | input: String, 35 | onActivityResult: ActivityResultCallback, 36 | onPermissionDenied: Callback1, 37 | onExplainRequestPermission: Callback0? = null 38 | ) { 39 | requestPermissionLauncher.launch( 40 | Manifest.permission.READ_EXTERNAL_STORAGE, 41 | onGranted = { launch(input, onActivityResult) }, 42 | onPermissionDenied, 43 | onExplainRequestPermission 44 | ) 45 | } 46 | 47 | @JvmOverloads 48 | fun launchForImage( 49 | onActivityResult: ActivityResultCallback, 50 | onPermissionDenied: Callback1, 51 | onExplainRequestPermission: Callback0? = null 52 | ) = launch("image/*", onActivityResult, onPermissionDenied, onExplainRequestPermission) 53 | 54 | @JvmOverloads 55 | fun launchForVideo( 56 | onActivityResult: ActivityResultCallback, 57 | onPermissionDenied: Callback1, 58 | onExplainRequestPermission: Callback0? = null 59 | ) = launch("video/*", onActivityResult, onPermissionDenied, onExplainRequestPermission) 60 | } 61 | 62 | class PickContentContract : ActivityResultContract() { 63 | override fun createIntent(context: Context, input: String) = 64 | Intent(Intent.ACTION_PICK).apply { type = input } 65 | 66 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? { 67 | return if (intent == null || resultCode != Activity.RESULT_OK) null else intent.data!! 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/RequestMultiplePermissionsLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import androidx.activity.result.ActivityResultCallback 22 | import androidx.activity.result.ActivityResultCaller 23 | import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions 24 | import com.dylanc.callbacks.Callback0 25 | import com.dylanc.callbacks.Callback1 26 | import com.dylanc.callbacks.Callback2 27 | import kotlinx.coroutines.flow.flow 28 | 29 | /** 30 | * @author Dylan Cai 31 | */ 32 | class RequestMultiplePermissionsLauncher(private val caller: ActivityResultCaller) : 33 | BaseActivityResultLauncher, Map>(caller, RequestMultiplePermissions()) { 34 | 35 | private val settingsLauncher = AppDetailsSettingsLauncher(caller) 36 | 37 | fun launch(vararg permissions: String, onActivityResult: ActivityResultCallback>) = 38 | launch(arrayOf(*permissions), onActivityResult) 39 | 40 | @JvmOverloads 41 | fun launch( 42 | vararg permissions: String, 43 | onAllGranted: Callback0, 44 | onDenied: Callback2, AppDetailsSettingsLauncher>, 45 | onExplainRequest: (Callback1>)? = null 46 | ) { 47 | launch(*permissions) { result -> 48 | if (result.containsValue(false)) { 49 | val deniedList = result.filter { !it.value }.map { it.key } 50 | val explainableList = deniedList.filter { caller.shouldShowRequestPermissionRationale(it) } 51 | if (explainableList.isNotEmpty()) { 52 | onExplainRequest?.invoke(explainableList) ?: onDenied(explainableList, settingsLauncher) 53 | } else { 54 | onDenied(deniedList, settingsLauncher) 55 | } 56 | } else { 57 | onAllGranted() 58 | } 59 | } 60 | } 61 | 62 | fun launchForFlow(vararg permissions: String) = launchForFlow(arrayOf(*permissions)) 63 | 64 | fun launchForFlow( 65 | vararg permissions: String, 66 | onDenied: Callback2, AppDetailsSettingsLauncher>, 67 | onExplainRequest: (Callback1>)? = null 68 | ) = flow { 69 | val result = launchForResult(arrayOf(*permissions)) 70 | if (result.containsValue(false)) { 71 | val deniedList = result.filter { !it.value }.map { it.key } 72 | val explainableList = deniedList.filter { caller.shouldShowRequestPermissionRationale(it) } 73 | if (explainableList.isNotEmpty()) { 74 | onExplainRequest?.invoke(explainableList) ?: onDenied(explainableList, settingsLauncher) 75 | } else { 76 | onDenied(deniedList, settingsLauncher) 77 | } 78 | } else { 79 | emit(Unit) 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/RequestPermissionLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import androidx.activity.result.ActivityResultCaller 22 | import androidx.activity.result.contract.ActivityResultContracts.RequestPermission 23 | import com.dylanc.callbacks.Callback0 24 | import com.dylanc.callbacks.Callback1 25 | import kotlinx.coroutines.flow.flow 26 | 27 | /** 28 | * @author Dylan Cai 29 | */ 30 | class RequestPermissionLauncher(private val caller: ActivityResultCaller) : 31 | BaseActivityResultLauncher(caller, RequestPermission()) { 32 | 33 | private val settingsLauncher = AppDetailsSettingsLauncher(caller) 34 | 35 | @JvmOverloads 36 | fun launch( 37 | permission: String, 38 | onGranted: Callback0, 39 | onDenied: Callback1, 40 | onExplainRequest: Callback0? = null 41 | ) { 42 | launch(permission) { 43 | when { 44 | it -> onGranted() 45 | caller.shouldShowRequestPermissionRationale(permission) -> 46 | onExplainRequest?.invoke() ?: onDenied(settingsLauncher) 47 | else -> onDenied(settingsLauncher) 48 | } 49 | } 50 | } 51 | 52 | fun launchForFlow( 53 | permission: String, 54 | onDenied: Callback1, 55 | onExplainRequest: Callback0? = null 56 | ) = flow { 57 | when { 58 | launchForResult(permission) -> emit(Unit) 59 | caller.shouldShowRequestPermissionRationale(permission) -> 60 | onExplainRequest?.invoke() ?: onDenied(settingsLauncher) 61 | else -> onDenied(settingsLauncher) 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/SaveAsLauncher.kt: -------------------------------------------------------------------------------- 1 | package com.dylanc.activityresult.launcher 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import androidx.activity.result.ActivityResultCaller 8 | import androidx.activity.result.contract.ActivityResultContract 9 | 10 | /** 11 | * @param type mimeType, text/plain etc. 12 | */ 13 | class SaveAsRequest( 14 | val fileName: String, 15 | val type: String, 16 | ) 17 | 18 | class SaveAsResultContract : ActivityResultContract() { 19 | override fun createIntent(context: Context, input: SaveAsRequest): Intent { 20 | return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 21 | addCategory(Intent.CATEGORY_OPENABLE) 22 | type = input.type 23 | putExtra(Intent.EXTRA_TITLE, input.fileName) 24 | } 25 | } 26 | 27 | override fun parseResult(resultCode: Int, intent: Intent?): Uri? { 28 | return if (resultCode == Activity.RESULT_OK && intent != null) { 29 | intent.data 30 | } else { 31 | null 32 | } 33 | } 34 | } 35 | 36 | class SaveAsLauncher(caller: ActivityResultCaller) : 37 | BaseActivityResultLauncher(caller, SaveAsResultContract()) -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/StartActivityLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.app.Activity 22 | import android.content.Intent 23 | import android.os.Bundle 24 | import androidx.activity.result.ActivityResult 25 | import androidx.activity.result.ActivityResultCaller 26 | import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult 27 | import androidx.core.os.bundleOf 28 | import com.dylanc.callbacks.Callback2 29 | 30 | /** 31 | * @author Dylan Cai 32 | */ 33 | class StartActivityLauncher(caller: ActivityResultCaller) : 34 | BaseActivityResultLauncher(caller, StartActivityForResult()) { 35 | 36 | inline fun launch(vararg pairs: Pair, onActivityResult: Callback2) { 37 | launch(T::class.java, bundleOf(*pairs), onActivityResult) 38 | } 39 | 40 | @JvmOverloads 41 | fun launch(clazz: Class, extras: Bundle? = null, onActivityResult: Callback2) { 42 | val intent = Intent(context, clazz) 43 | extras?.let { intent.putExtras(it) } 44 | launch(intent, onActivityResult) 45 | } 46 | 47 | fun launch(intent: Intent, onActivityResult: Callback2) = 48 | launch(intent) { result -> onActivityResult(result.resultCode, result.data) } 49 | 50 | suspend inline fun launchForResult(vararg pairs: Pair) = 51 | launchForResult(bundleOf(*pairs)) 52 | 53 | suspend inline fun launchForResult(extras: Bundle? = null) = 54 | launchForResult(Intent(extras)) 55 | 56 | inline fun launchForFlow(vararg pairs: Pair) = 57 | launchForFlow(bundleOf(*pairs)) 58 | 59 | inline fun launchForFlow(extras: Bundle? = null) = 60 | launchForFlow(Intent(extras)) 61 | 62 | inline fun Intent(extras: Bundle? = null) = 63 | Intent(context, T::class.java).apply { extras?.let { putExtras(it) } } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/StartIntentSenderLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.annotation.SuppressLint 22 | import android.content.Intent 23 | import android.content.Intent.* 24 | import android.content.IntentSender 25 | import androidx.activity.result.ActivityResult 26 | import androidx.activity.result.ActivityResultCaller 27 | import androidx.activity.result.IntentSenderRequest 28 | import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult 29 | import androidx.annotation.IntDef 30 | import com.dylanc.callbacks.Callback2 31 | 32 | /** 33 | * @author Dylan Cai 34 | */ 35 | class StartIntentSenderLauncher(caller: ActivityResultCaller) : 36 | BaseActivityResultLauncher(caller, StartIntentSenderForResult()) { 37 | 38 | @JvmOverloads 39 | fun launch( 40 | intentSender: IntentSender, 41 | fillInIntent: Intent? = null, 42 | @Flag flagsValues: Int = 0, 43 | flagsMask: Int = 0, 44 | onActivityResult: Callback2 45 | ) = 46 | launch(IntentSenderRequest(intentSender, fillInIntent, flagsValues, flagsMask)) { 47 | onActivityResult(it.resultCode, it.data) 48 | } 49 | 50 | suspend fun launchForResult( 51 | intentSender: IntentSender, 52 | fillInIntent: Intent? = null, 53 | @Flag flagsValues: Int = 0, 54 | flagsMask: Int = 0 55 | ) = 56 | launchForResult(IntentSenderRequest(intentSender, fillInIntent, flagsValues, flagsMask)) 57 | 58 | private fun IntentSenderRequest( 59 | intentSender: IntentSender, 60 | fillInIntent: Intent? = null, 61 | @Flag flagsValues: Int = 0, 62 | flagsMask: Int = 0 63 | ) = IntentSenderRequest.Builder(intentSender) 64 | .setFillInIntent(fillInIntent) 65 | .setFlags(flagsValues, flagsMask) 66 | .build() 67 | 68 | @SuppressLint("InlinedApi") 69 | @IntDef( 70 | FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION, 71 | FLAG_FROM_BACKGROUND, FLAG_DEBUG_LOG_RESOLUTION, FLAG_EXCLUDE_STOPPED_PACKAGES, 72 | FLAG_INCLUDE_STOPPED_PACKAGES, FLAG_GRANT_PERSISTABLE_URI_PERMISSION, 73 | FLAG_GRANT_PREFIX_URI_PERMISSION, FLAG_ACTIVITY_MATCH_EXTERNAL, 74 | FLAG_ACTIVITY_NO_HISTORY, FLAG_ACTIVITY_SINGLE_TOP, FLAG_ACTIVITY_NEW_TASK, 75 | FLAG_ACTIVITY_MULTIPLE_TASK, FLAG_ACTIVITY_CLEAR_TOP, 76 | FLAG_ACTIVITY_FORWARD_RESULT, FLAG_ACTIVITY_PREVIOUS_IS_TOP, 77 | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS, FLAG_ACTIVITY_BROUGHT_TO_FRONT, 78 | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY, 79 | FLAG_ACTIVITY_NEW_DOCUMENT, FLAG_ACTIVITY_NO_USER_ACTION, 80 | FLAG_ACTIVITY_REORDER_TO_FRONT, FLAG_ACTIVITY_NO_ANIMATION, 81 | FLAG_ACTIVITY_CLEAR_TASK, FLAG_ACTIVITY_TASK_ON_HOME, 82 | FLAG_ACTIVITY_RETAIN_IN_RECENTS, FLAG_ACTIVITY_LAUNCH_ADJACENT 83 | ) 84 | @Retention(AnnotationRetention.SOURCE) 85 | private annotation class Flag 86 | } -------------------------------------------------------------------------------- /app/src/main/java/com/dylanc/activityresult/launcher/TakePicturePreviewLauncher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021. Dylan Cai 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("unused") 18 | 19 | package com.dylanc.activityresult.launcher 20 | 21 | import android.graphics.Bitmap 22 | import androidx.activity.result.ActivityResultCallback 23 | import androidx.activity.result.ActivityResultCaller 24 | import androidx.activity.result.contract.ActivityResultContracts.TakePicturePreview 25 | 26 | /** 27 | * @author Dylan Cai 28 | */ 29 | class TakePicturePreviewLauncher(caller: ActivityResultCaller) : 30 | BaseActivityResultLauncher(caller, TakePicturePreview()) { 31 | 32 | fun launch(onActivityResult: ActivityResultCallback) = launch(null, onActivityResult) 33 | 34 | suspend fun launchForResult() = launchForResult(null) 35 | 36 | fun launchForFlow() = launchForFlow(null) 37 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/App.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED") 2 | 3 | package com.salt.video 4 | 5 | import android.annotation.SuppressLint 6 | import android.app.Application 7 | import android.content.Context 8 | import androidx.annotation.Keep 9 | import coil.ImageLoader 10 | import coil.ImageLoaderFactory 11 | import coil.decode.VideoFrameDecoder 12 | import com.kongzue.dialogx.DialogX 13 | import com.kongzue.dialogx.style.IOSStyle 14 | import com.salt.video.data.AppDatabase 15 | import com.tencent.mmkv.MMKV 16 | 17 | class App: Application(), ImageLoaderFactory { 18 | 19 | override fun onCreate() { 20 | super.onCreate() 21 | context = applicationContext 22 | initMMKV() 23 | initDialogX() 24 | initAppDatabase() 25 | } 26 | 27 | private fun initMMKV() { 28 | MMKV.initialize(context) 29 | mmkv = MMKV.defaultMMKV()!! 30 | } 31 | 32 | /** 初始化 DialogX */ 33 | private fun initDialogX() { 34 | DialogX.init(this) 35 | DialogX.globalStyle = IOSStyle() 36 | DialogX.globalTheme = DialogX.THEME.AUTO 37 | } 38 | 39 | /** 初始化数据库 */ 40 | private fun initAppDatabase() { 41 | appDatabase = AppDatabase.getDatabase(applicationContext) 42 | } 43 | 44 | override fun newImageLoader(): ImageLoader { 45 | return ImageLoader.Builder(this) 46 | .components { 47 | add(VideoFrameDecoder.Factory()) 48 | } 49 | .build() 50 | } 51 | 52 | /** 53 | * 屏蔽魅族 Flyme 反色 54 | */ 55 | @Keep 56 | fun mzNightModeUseOf(): Int = 2 57 | 58 | companion object { 59 | 60 | @SuppressLint("StaticFieldLeak") 61 | lateinit var context: Context 62 | private set 63 | 64 | lateinit var mmkv: MMKV 65 | private set 66 | 67 | lateinit var appDatabase: AppDatabase 68 | private set 69 | 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/core/PlayerState.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.core 2 | 3 | enum class PlayerState { 4 | RESUME, 5 | PAUSE 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/core/SaltVideoPlayer.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.core 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Log 6 | import android.view.GestureDetector 7 | import android.view.GestureDetector.SimpleOnGestureListener 8 | import android.view.MotionEvent 9 | import com.salt.video.App 10 | import com.salt.video.R 11 | import com.salt.video.util.Config 12 | import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer 13 | 14 | /** 15 | * SAR 表示 Sample Aspect Ratio,含义是采样宽高比。 16 | * DAR(Display Aspect Ratio)表示显示宽高比,也就是我们经常说的 16:9,4:3。 17 | */ 18 | class SaltVideoPlayer: StandardGSYVideoPlayer { 19 | constructor(context: Context?, fullFlag: Boolean?) : super(context, fullFlag) 20 | constructor(context: Context?) : super(context) 21 | constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) 22 | 23 | /** 24 | * @param 25 | */ 26 | var onVideoSizeChangeListener: (width: Int, height: Int, numerator: Int, denominator: Int) -> Unit = { _, _, _, _ -> } 27 | 28 | var onClickUiToggle: () -> Unit = {} 29 | 30 | var onPlayerStateChange: (PlayerState) -> Unit = {} 31 | 32 | var onSetProgressAndTime: (currentTime: Long, totalTime: Long) -> Unit = { _, _ -> } 33 | 34 | var onLongTouchDown: () -> Unit = {} 35 | var onLongTouchUp: () -> Unit = {} 36 | 37 | var onPrepared: () -> Unit = {} 38 | 39 | /** 是否正在使用手势拖动 */ 40 | private var isDragSeeking = false 41 | 42 | override fun onPrepared() { 43 | super.onPrepared() 44 | onPrepared.invoke() 45 | } 46 | 47 | override fun startAfterPrepared() { 48 | super.startAfterPrepared() 49 | } 50 | 51 | override fun getLayoutId(): Int { 52 | return R.layout.layout_salt_video_player 53 | } 54 | 55 | override fun touchDoubleUp(e: MotionEvent?) { 56 | super.touchDoubleUp(e) 57 | } 58 | 59 | override fun showProgressDialog(deltaX: Float, seekTime: String?, seekTimePosition: Long, totalTime: String?, totalTimeDuration: Long) { 60 | super.showProgressDialog(deltaX, seekTime, seekTimePosition, totalTime, totalTimeDuration) 61 | isDragSeeking = true 62 | onSetProgressAndTime(seekTimePosition, totalTimeDuration) 63 | Log.d(TAG, "showProgressDialog seekTime = $seekTime, seekTimePosition = $seekTimePosition, totalTime = $totalTime, totalTimeDuration = $totalTimeDuration") 64 | } 65 | 66 | override fun dismissProgressDialog() { 67 | super.dismissProgressDialog() 68 | isDragSeeking = false 69 | } 70 | 71 | override fun resolveUIState(state: Int) { 72 | super.resolveUIState(state) 73 | when(state) { 74 | CURRENT_STATE_PLAYING -> onPlayerStateChange(PlayerState.RESUME) 75 | CURRENT_STATE_PAUSE -> onPlayerStateChange(PlayerState.PAUSE) 76 | CURRENT_STATE_AUTO_COMPLETE -> onVideoReset() 77 | } 78 | } 79 | 80 | override fun onVideoSizeChanged() { 81 | super.onVideoSizeChanged() 82 | onVideoSizeChangeListener.invoke(currentVideoWidth, currentVideoHeight, videoSarNum, videoSarDen) 83 | } 84 | 85 | override fun onClickUiToggle(e: MotionEvent?) { 86 | super.onClickUiToggle(e) 87 | onClickUiToggle() 88 | } 89 | 90 | override fun init(context: Context?) { 91 | super.init(context) 92 | gestureDetector = GestureDetector(getContext().applicationContext, object : SimpleOnGestureListener() { 93 | override fun onDoubleTap(e: MotionEvent): Boolean { 94 | touchDoubleUp(e) 95 | return super.onDoubleTap(e) 96 | } 97 | 98 | override fun onSingleTapConfirmed(e: MotionEvent): Boolean { 99 | if (!mChangePosition && !mChangeVolume && !mBrightness) { 100 | onClickUiToggle(e) 101 | } 102 | return super.onSingleTapConfirmed(e) 103 | } 104 | 105 | override fun onLongPress(e: MotionEvent) { 106 | super.onLongPress(e) 107 | touchLongPress(e) 108 | } 109 | }) 110 | } 111 | 112 | /** 是否进行了长按倍数播放 */ 113 | private var isLongPressSpeed = false 114 | 115 | override fun touchLongPress(e: MotionEvent?) { 116 | super.touchLongPress(e) 117 | onLongTouchDown() 118 | isLongPressSpeed = true 119 | speed = 2f 120 | Log.d(TAG, "touchLongPress(e)") 121 | } 122 | 123 | override fun touchSurfaceUp() { 124 | super.touchSurfaceUp() 125 | if (isLongPressSpeed) { 126 | isLongPressSpeed = false 127 | onLongTouchUp() 128 | } 129 | Log.d(TAG, "touchSurfaceUp()") 130 | } 131 | 132 | // override fun getProgressDialogLayoutId(): Int { 133 | // return super.getProgressDialogLayoutId() 134 | // } 135 | 136 | 137 | init { 138 | setGSYVideoProgressListener { progress, secProgress, currentPosition, duration -> 139 | Log.d(TAG, "p: $progress, sp: $secProgress, c: $currentPosition, d: $duration") 140 | if (!isDragSeeking) { 141 | onSetProgressAndTime(currentPosition, duration) 142 | } 143 | } 144 | isLooping = true 145 | isReleaseWhenLossAudio = false 146 | } 147 | 148 | companion object { 149 | private const val TAG = "SaltVideoPlayer" 150 | } 151 | 152 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/data/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.data 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import com.salt.video.data.dao.MediaSourceDao 8 | import com.salt.video.data.dao.VideoDao 9 | import com.salt.video.data.entry.MediaSource 10 | import com.salt.video.data.entry.Video 11 | 12 | /** 13 | * The Room database for this app 14 | */ 15 | @Database( 16 | entities = [ 17 | MediaSource::class, 18 | Video::class 19 | ], 20 | version = 1, 21 | exportSchema = false 22 | ) 23 | abstract class AppDatabase : RoomDatabase() { 24 | 25 | abstract fun mediaSourceDao(): MediaSourceDao 26 | abstract fun videoDao(): VideoDao 27 | 28 | companion object { 29 | // For Singleton instantiation 30 | @Volatile 31 | private var instance: AppDatabase? = null 32 | 33 | // private val MIGRATION_1_2 = object : Migration(1, 2) { 34 | // override fun migrate(database: SupportSQLiteDatabase) { 35 | // database.execSQL("ALTER TABLE Song ADD COLUMN playedTimes INTEGER NOT NULL DEFAULT 0") 36 | // 37 | // database.execSQL("CREATE TABLE Listening (id TEXT NOT NULL PRIMARY KEY, year INTEGER NOT NULL DEFAULT 0, month INTEGER NOT NULL DEFAULT 0, day INTEGER NOT NULL DEFAULT 0, hour INTEGER NOT NULL DEFAULT 0, minute INTEGER NOT NULL DEFAULT 0, songId TEXT NOT NULL DEFAULT \"\", duration INTEGER NOT NULL DEFAULT 0)") 38 | // } 39 | // } 40 | 41 | @Synchronized 42 | fun getDatabase(context: Context): AppDatabase { 43 | instance?.let { 44 | return it 45 | } 46 | return Room.databaseBuilder( 47 | context.applicationContext, 48 | AppDatabase::class.java, DB_NAME 49 | ) 50 | // .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7) 51 | .fallbackToDestructiveMigration() // 上线移除 52 | .build().also { 53 | instance = it 54 | } 55 | } 56 | 57 | const val DB_NAME = "app_database" 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/data/dao/MediaSourceDao.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.data.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import com.salt.video.data.entry.MediaSource 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | @Dao 11 | interface MediaSourceDao { 12 | 13 | @Insert(onConflict = OnConflictStrategy.IGNORE) 14 | suspend fun insert(mediaSource: MediaSource) 15 | 16 | @Query("SELECT * FROM MediaSource") 17 | fun getAll(): Flow> 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/data/dao/VideoDao.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.data.dao 2 | 3 | import androidx.room.Dao 4 | 5 | @Dao 6 | interface VideoDao { 7 | 8 | 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/data/entry/MediaSource.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.data.entry 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | /** 7 | * 媒体来源 8 | */ 9 | @Entity 10 | data class MediaSource ( 11 | 12 | /** 13 | * 唯一路径 14 | */ 15 | @PrimaryKey 16 | val url: String 17 | 18 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/data/entry/Video.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.data.entry 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | /** 7 | * 视频数据量 8 | */ 9 | @Entity 10 | data class Video( 11 | 12 | /** 唯一标识主键 */ 13 | @PrimaryKey 14 | val url: String, 15 | 16 | /** 标题 */ 17 | val title: String, 18 | 19 | /** 编辑时间 */ 20 | val dateModified: Long 21 | 22 | ) 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/data/ext/VideoExt.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.data.ext 2 | 3 | import android.provider.DocumentsContract 4 | import androidx.documentfile.provider.DocumentFile 5 | import com.salt.video.data.entry.Video 6 | 7 | //fun DocumentFile.toVideo(): Video { 8 | // return Video( 9 | // url = uri.toString(), 10 | // title = name ?: "" 11 | // ) 12 | //} 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/data/fui/MediaSourceFui.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.data.fui 2 | 3 | import android.content.Context 4 | import android.provider.DocumentsContract 5 | import androidx.core.net.toUri 6 | import androidx.documentfile.provider.DocumentFile 7 | import com.salt.video.data.entry.MediaSource 8 | 9 | data class MediaSourceFui( 10 | val title: String, 11 | val directoryUri: String, 12 | val mediaSource: MediaSource 13 | ) 14 | 15 | fun MediaSource.toFui( 16 | context: Context 17 | ): MediaSourceFui { 18 | val treeUri = url.toUri() 19 | val documentFile = DocumentFile.fromTreeUri(context, treeUri) 20 | var directoryUri = "" 21 | if (documentFile != null) { 22 | // mediaSource 是 treeUri 23 | // 调用路径页面需要传入普通的 uri (非 treeUri) 24 | val documentId = DocumentsContract.getDocumentId(documentFile.uri) 25 | directoryUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId).toString() 26 | } 27 | 28 | return MediaSourceFui( 29 | title = documentFile?.name ?: "", 30 | directoryUri = directoryUri, 31 | mediaSource = this 32 | ) 33 | } 34 | 35 | fun List.toFuiList( 36 | context: Context 37 | ): List { 38 | val list = ArrayList() 39 | this.forEach { 40 | list.add(it.toFui(context)) 41 | } 42 | return list 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/data/repo/MediaSourceRepo.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.data.repo 2 | 3 | import com.salt.video.App 4 | import com.salt.video.data.entry.MediaSource 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | object MediaSourceRepo { 8 | 9 | suspend fun insert(mediaSource: MediaSource) = dao.insert(mediaSource) 10 | 11 | fun getAll(): Flow> = dao.getAll() 12 | 13 | private val dao = App.appDatabase.mediaSourceDao() 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/ui/about/AboutActivity.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.ui.about 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.material.Icon 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.clip 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.res.painterResource 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.unit.dp 28 | import com.moriafly.salt.ui.Item 29 | import com.moriafly.salt.ui.ItemSpacer 30 | import com.moriafly.salt.ui.ItemText 31 | import com.moriafly.salt.ui.ItemTitle 32 | import com.moriafly.salt.ui.RoundedColumn 33 | import com.moriafly.salt.ui.SaltTheme 34 | import com.moriafly.salt.ui.SaltUILogo 35 | import com.moriafly.salt.ui.UnstableSaltApi 36 | import com.salt.video.BuildConfig 37 | import com.salt.video.R 38 | import com.salt.video.ui.base.BasicActivityColumn 39 | import com.salt.video.ui.main.my.OpenSourceDialog 40 | import com.salt.video.ui.theme.VideoTheme 41 | 42 | class AboutActivity: AppCompatActivity() { 43 | 44 | override fun onCreate(savedInstanceState: Bundle?) { 45 | super.onCreate(savedInstanceState) 46 | setContent { 47 | VideoTheme { 48 | AboutUI() 49 | } 50 | } 51 | } 52 | 53 | } 54 | 55 | @OptIn(UnstableSaltApi::class) 56 | @Composable 57 | private fun AboutUI() { 58 | BasicActivityColumn( 59 | text = stringResource(id = R.string.about) 60 | ) { 61 | RoundedColumn { 62 | ItemTitle(text = "法律信息") 63 | // Item( 64 | // onClick = { 65 | // 66 | // }, 67 | // text = "软件使用条例", 68 | // iconPainter = painterResource(id = R.drawable.ic_license), 69 | // iconColor = SaltTheme.colors.highlight 70 | // ) 71 | // 72 | // Item( 73 | // onClick = { 74 | // 75 | // }, 76 | // text = "隐私协议", 77 | // iconPainter = painterResource(id = R.drawable.ic_license), 78 | // iconColor = SaltTheme.colors.highlight 79 | // ) 80 | 81 | var openSourceDialog by remember { mutableStateOf(false) } 82 | Item( 83 | onClick = { 84 | openSourceDialog = true 85 | }, 86 | text = "开放源代码许可", 87 | iconPainter = painterResource(id = R.drawable.ic_copyright), 88 | iconColor = SaltTheme.colors.highlight 89 | ) 90 | if (openSourceDialog) { 91 | OpenSourceDialog( 92 | onDismissRequest = { 93 | openSourceDialog = false 94 | } 95 | ) 96 | } 97 | } 98 | 99 | RoundedColumn { 100 | ItemTitle(text = "我们") 101 | 102 | ItemSpacer() 103 | ItemText(text = "此为开发体验版本,所有功能都可能在后续版本变更或移除,加入 QQ 群聊 639298754") 104 | ItemSpacer() 105 | } 106 | 107 | SaltUILogo() 108 | 109 | Column( 110 | modifier = Modifier 111 | .fillMaxWidth(), 112 | horizontalAlignment = Alignment.CenterHorizontally 113 | ) { 114 | ItemSpacer() 115 | Icon( 116 | painter = painterResource(id = R.drawable.ic_salt_video), 117 | contentDescription = null, 118 | modifier = Modifier 119 | .size(40.dp) 120 | .clip(CircleShape) 121 | .background(color = SaltTheme.colors.highlight) 122 | .padding(10.dp), 123 | tint = Color.White 124 | ) 125 | ItemSpacer() 126 | Text( 127 | text = "${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE})", 128 | style = SaltTheme.textStyles.sub 129 | ) 130 | Spacer(modifier = Modifier.height(2.dp)) 131 | Text( 132 | text = "Copyright © 2022-2023 Moriafly. All Rights Reserved.", 133 | style = SaltTheme.textStyles.sub 134 | ) 135 | ItemSpacer() 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/ui/base/BasicActivityColumn.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.ui.base 2 | 3 | import android.app.Activity 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.ColumnScope 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.navigationBarsPadding 12 | import androidx.compose.foundation.layout.statusBarsPadding 13 | import androidx.compose.foundation.rememberScrollState 14 | import androidx.compose.foundation.verticalScroll 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.unit.dp 19 | import com.moriafly.salt.ui.SaltTheme 20 | import com.moriafly.salt.ui.TitleBar 21 | import com.moriafly.salt.ui.UnstableSaltApi 22 | 23 | @OptIn(UnstableSaltApi::class) 24 | @Composable 25 | fun BasicActivityColumn( 26 | text: String, 27 | content: @Composable ColumnScope.() -> Unit 28 | ) { 29 | Column( 30 | modifier = Modifier 31 | .fillMaxWidth() 32 | .statusBarsPadding() 33 | .navigationBarsPadding() 34 | ) { 35 | val activity = LocalContext.current as Activity 36 | Box( 37 | modifier = Modifier 38 | .fillMaxWidth() 39 | .height(56.dp) 40 | ) { 41 | TitleBar( 42 | onBack = { 43 | activity.finish() 44 | }, 45 | text = text, 46 | showBackBtn = true 47 | ) 48 | } 49 | Column( 50 | modifier = Modifier 51 | .weight(1f) 52 | .fillMaxSize() 53 | .verticalScroll(rememberScrollState()) 54 | .background(SaltTheme.colors.background) 55 | ) { 56 | content() 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/ui/base/LazyFragment.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.ui.base 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | /** 6 | * 延迟加载 Fragment 7 | */ 8 | abstract class LazyFragment: Fragment() { 9 | 10 | var isLoaded = false 11 | private set 12 | 13 | override fun onResume() { 14 | super.onResume() 15 | if (!isLoaded && !isHidden) { 16 | lazyInit() 17 | isLoaded = true 18 | } 19 | } 20 | 21 | override fun onDestroyView() { 22 | super.onDestroyView() 23 | isLoaded = false 24 | } 25 | 26 | abstract fun lazyInit() 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/ui/dialogx/XSMStyle.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.ui.dialogx 2 | 3 | import com.kongzue.dialogx.style.IOSStyle 4 | import com.salt.video.R 5 | 6 | class XSMStyle: IOSStyle() { 7 | 8 | override fun layout(light: Boolean): Int { 9 | return R.layout.layout_dialogx_xsm_dark 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/ui/localfolder/LocalFolder.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.ui.localfolder 2 | 3 | import android.net.Uri 4 | import android.provider.DocumentsContract 5 | import android.util.Log 6 | import androidx.documentfile.provider.DocumentFile 7 | 8 | data class LocalFolder( 9 | val name: String, 10 | val url: String, 11 | val dateModified: Long 12 | ) 13 | 14 | //fun DocumentFile.toLocalFolder(treeUri: Uri): LocalFolder { 15 | // val documentId = DocumentsContract.getDocumentId(uri) 16 | // val url = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId).toString() 17 | // Log.d("DocumentFile.toLocalFolder", "documentId = $documentId, url = $url") 18 | // return LocalFolder( 19 | // name = name ?: "", 20 | // url = url, 21 | // dateModified = this. 22 | // ) 23 | //} 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/salt/video/ui/localfolder/LocalFolderViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.salt.video.ui.localfolder 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.database.Cursor 6 | import android.provider.DocumentsContract 7 | import androidx.core.database.getStringOrNull 8 | import androidx.core.net.toUri 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import com.salt.video.App 12 | import com.salt.video.data.entry.Video 13 | import com.salt.video.util.Config 14 | import com.salt.video.util.DocumentFileUtil 15 | import com.salt.video.util.pinyinString 16 | import com.salt.video.util.sort.SimpleNaturalComparator 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.flow.MutableStateFlow 19 | import kotlinx.coroutines.flow.asStateFlow 20 | import kotlinx.coroutines.launch 21 | 22 | class LocalFolderViewModel: ViewModel() { 23 | 24 | private val _title = MutableStateFlow(null) 25 | val title = _title.asStateFlow() 26 | 27 | private val _files = MutableStateFlow?>(null) 28 | val files = _files.asStateFlow() 29 | 30 | fun load(context: Context, treeUriPath: String) { 31 | viewModelScope.launch(Dispatchers.IO) { 32 | val treeUri = treeUriPath.toUri() 33 | 34 | val documentFile = DocumentFileUtil.getDocumentFileFormTreeUri(context, treeUri) 35 | if (documentFile != null) { 36 | _title.emit(documentFile.name) 37 | } 38 | 39 | val folders = ArrayList() 40 | val videos = ArrayList