├── .gitignore ├── FileExplorer ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── src │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── magicianguo │ │ │ └── fileexplorer │ │ │ └── ExampleInstrumentedTest.kt │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── aidl │ │ │ └── com │ │ │ │ └── magicianguo │ │ │ │ └── fileexplorer │ │ │ │ ├── bean │ │ │ │ └── BeanFile.aidl │ │ │ │ └── userservice │ │ │ │ └── IFileExplorerService.aidl │ │ ├── java │ │ │ └── com │ │ │ │ └── magicianguo │ │ │ │ └── fileexplorer │ │ │ │ ├── App.java │ │ │ │ ├── activity │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ └── SettingActivity.java │ │ │ │ ├── adapter │ │ │ │ └── FileListAdapter.java │ │ │ │ ├── bean │ │ │ │ └── BeanFile.java │ │ │ │ ├── constant │ │ │ │ ├── BundleKey.java │ │ │ │ ├── PathType.java │ │ │ │ └── RequestCode.java │ │ │ │ ├── fragment │ │ │ │ ├── EmptyFragment.java │ │ │ │ └── FileListFragment.java │ │ │ │ ├── observer │ │ │ │ └── IFileItemClickObserver.java │ │ │ │ ├── userservice │ │ │ │ ├── FileExplorerService.java │ │ │ │ └── FileExplorerServiceManager.java │ │ │ │ └── util │ │ │ │ ├── FileTools.java │ │ │ │ ├── PermissionTools.java │ │ │ │ ├── SPUtils.java │ │ │ │ └── ToastUtils.java │ │ └── res │ │ │ ├── drawable │ │ │ ├── bg_file_item.xml │ │ │ ├── bg_icon_dir.xml │ │ │ ├── bg_icon_file.xml │ │ │ └── bg_tv_path.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ ├── activity_setting.xml │ │ │ ├── fragment_file_list.xml │ │ │ └── layout_file_list_item.xml │ │ │ ├── menu │ │ │ └── menu_main.xml │ │ │ ├── mipmap │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── navigation │ │ │ └── nav_main_file_list.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ └── test │ │ └── java │ │ └── com │ │ └── magicianguo │ │ └── fileexplorer │ │ └── ExampleUnitTest.kt ├── testks-sign.gradle └── testks.jks ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme_pic ├── android11_data.png ├── android11_data_confirm.png ├── android13_data.png ├── android13_data_request.png └── android13_data_tips.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /FileExplorer/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /FileExplorer/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | apply from: "testks-sign.gradle" 7 | 8 | android { 9 | namespace 'com.magicianguo.fileexplorer' 10 | compileSdk 34 11 | 12 | defaultConfig { 13 | applicationId "com.magicianguo.fileexplorer" 14 | minSdk 21 15 | targetSdk 34 16 | versionCode 1 17 | versionName "1.2" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_1_8 23 | targetCompatibility JavaVersion.VERSION_1_8 24 | } 25 | kotlinOptions { 26 | jvmTarget = '1.8' 27 | } 28 | buildFeatures { 29 | buildConfig true 30 | aidl true 31 | viewBinding true 32 | } 33 | } 34 | 35 | dependencies { 36 | 37 | implementation 'androidx.core:core-ktx:1.10.1' 38 | implementation 'com.google.android.material:material:1.10.0' 39 | 40 | def shizuku_version = "13.1.5" 41 | implementation "dev.rikka.shizuku:api:$shizuku_version" 42 | // Add this line if you want to support Shizuku 43 | implementation "dev.rikka.shizuku:provider:$shizuku_version" 44 | // 一定要加上这条依赖,SDK自带的documentfile接口有问题 45 | implementation "androidx.documentfile:documentfile:1.0.1" 46 | def nav_version = "2.7.7" 47 | // Java language implementation 48 | implementation "androidx.navigation:navigation-fragment:$nav_version" 49 | implementation "androidx.navigation:navigation-ui:$nav_version" 50 | } -------------------------------------------------------------------------------- /FileExplorer/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 | -keep class com.magicianguo.fileexplorer.bean.BeanFile { *; } 24 | -keep class com.magicianguo.fileexplorer.userservice.IFileExplorerService { *; } 25 | -keep class com.magicianguo.fileexplorer.userservice.FileExplorerService -------------------------------------------------------------------------------- /FileExplorer/src/androidTest/java/com/magicianguo/fileexplorer/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | //package com.magicianguo.fileexplorer 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.magicianguo.fileexplorer", appContext.packageName) 23 | // } 24 | //} -------------------------------------------------------------------------------- /FileExplorer/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 26 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 49 | 50 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /FileExplorer/src/main/aidl/com/magicianguo/fileexplorer/bean/BeanFile.aidl: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.bean; 2 | 3 | import com.magicianguo.fileexplorer.bean.BeanFile; 4 | 5 | parcelable BeanFile; -------------------------------------------------------------------------------- /FileExplorer/src/main/aidl/com/magicianguo/fileexplorer/userservice/IFileExplorerService.aidl: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.userservice; 2 | 3 | import com.magicianguo.fileexplorer.bean.BeanFile; 4 | 5 | interface IFileExplorerService { 6 | List listFiles(String path); 7 | } -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/App.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.os.Bundle; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | 10 | import java.io.File; 11 | import java.io.FileOutputStream; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class App extends Application { 16 | private static App sApp; 17 | private static final List sAliveActivityList = new ArrayList<>(); 18 | 19 | public static App get() { 20 | return sApp; 21 | } 22 | 23 | @Override 24 | public void onCreate() { 25 | super.onCreate(); 26 | sApp = this; 27 | createFile(); 28 | registerAliveActivityList(); 29 | } 30 | 31 | private void createFile() { 32 | try { 33 | String path = getExternalFilesDir(null).getParent(); 34 | File file = new File(path, "test.txt"); 35 | if (!file.exists()) { 36 | file.createNewFile(); 37 | FileOutputStream fileOutputStream = new FileOutputStream(file); 38 | fileOutputStream.write("Hello world!".getBytes()); 39 | fileOutputStream.close(); 40 | } 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | } 44 | } 45 | 46 | public static List getAliveActivityList() { 47 | return sAliveActivityList; 48 | } 49 | 50 | private void registerAliveActivityList() { 51 | registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { 52 | @Override 53 | public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { 54 | sAliveActivityList.add(activity); 55 | } 56 | 57 | @Override 58 | public void onActivityStarted(@NonNull Activity activity) { 59 | } 60 | 61 | @Override 62 | public void onActivityResumed(@NonNull Activity activity) { 63 | } 64 | 65 | @Override 66 | public void onActivityPaused(@NonNull Activity activity) { 67 | } 68 | 69 | @Override 70 | public void onActivityStopped(@NonNull Activity activity) { 71 | } 72 | 73 | @Override 74 | public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { 75 | } 76 | 77 | @Override 78 | public void onActivityDestroyed(@NonNull Activity activity) { 79 | sAliveActivityList.remove(activity); 80 | } 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/activity/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.activity; 2 | 3 | import android.content.Intent; 4 | import android.content.pm.PackageManager; 5 | import android.net.Uri; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.os.Environment; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | import androidx.appcompat.app.AppCompatActivity; 13 | import androidx.viewbinding.ViewBinding; 14 | 15 | import com.magicianguo.fileexplorer.constant.PathType; 16 | import com.magicianguo.fileexplorer.constant.RequestCode; 17 | import com.magicianguo.fileexplorer.userservice.FileExplorerServiceManager; 18 | import com.magicianguo.fileexplorer.util.FileTools; 19 | import com.magicianguo.fileexplorer.util.PermissionTools; 20 | 21 | import rikka.shizuku.Shizuku; 22 | 23 | public abstract class BaseActivity extends AppCompatActivity 24 | implements Shizuku.OnRequestPermissionResultListener { 25 | protected final String TAG = this.getClass().getSimpleName(); 26 | protected VB binding; 27 | 28 | @Override 29 | protected void onCreate(@Nullable Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | binding = onBinding(); 32 | setContentView(binding.getRoot()); 33 | if (PermissionTools.isShizukuAvailable()) { 34 | Shizuku.addRequestPermissionResultListener(this); 35 | } 36 | } 37 | 38 | @Override 39 | protected void onDestroy() { 40 | super.onDestroy(); 41 | if (PermissionTools.isShizukuAvailable()) { 42 | Shizuku.removeRequestPermissionResultListener(this); 43 | } 44 | binding = null; 45 | } 46 | 47 | @Override 48 | public void onRequestPermissionResult(int requestCode, int grantResult) { 49 | if (requestCode == RequestCode.SHIZUKU) { 50 | if (grantResult == PackageManager.PERMISSION_GRANTED) { 51 | FileTools.specialPathReadType = PathType.SHIZUKU; 52 | FileExplorerServiceManager.bindService(); 53 | } 54 | } 55 | } 56 | 57 | @Override 58 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 59 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 60 | if (requestCode == RequestCode.STORAGE) { 61 | onStoragePermissionResult(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED); 62 | } 63 | } 64 | 65 | @Override 66 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 67 | super.onActivityResult(requestCode, resultCode, data); 68 | if (requestCode == RequestCode.STORAGE) { 69 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 70 | onStoragePermissionResult(Environment.isExternalStorageManager()); 71 | } 72 | } else if (requestCode == RequestCode.DOCUMENT) { 73 | Uri uri; 74 | if (data != null && (uri = data.getData()) != null) { 75 | getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 76 | onDocumentPermissionResult(true); 77 | } else { 78 | onDocumentPermissionResult(false); 79 | } 80 | } 81 | } 82 | 83 | protected void onStoragePermissionResult(boolean granted) { 84 | } 85 | 86 | protected void onDocumentPermissionResult(boolean granted) { 87 | } 88 | 89 | protected abstract VB onBinding(); 90 | } 91 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.activity; 2 | 3 | import android.content.ClipData; 4 | import android.content.ClipDescription; 5 | import android.content.ClipboardManager; 6 | import android.content.Intent; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.os.Parcelable; 10 | import android.text.TextUtils; 11 | import android.view.Menu; 12 | import android.view.MenuItem; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.annotation.Nullable; 16 | import androidx.appcompat.app.AlertDialog; 17 | import androidx.navigation.NavController; 18 | import androidx.navigation.fragment.NavHostFragment; 19 | 20 | import com.magicianguo.fileexplorer.R; 21 | import com.magicianguo.fileexplorer.bean.BeanFile; 22 | import com.magicianguo.fileexplorer.constant.BundleKey; 23 | import com.magicianguo.fileexplorer.constant.PathType; 24 | import com.magicianguo.fileexplorer.databinding.ActivityMainBinding; 25 | import com.magicianguo.fileexplorer.observer.IFileItemClickObserver; 26 | import com.magicianguo.fileexplorer.userservice.FileExplorerServiceManager; 27 | import com.magicianguo.fileexplorer.util.FileTools; 28 | import com.magicianguo.fileexplorer.util.PermissionTools; 29 | import com.magicianguo.fileexplorer.util.SPUtils; 30 | import com.magicianguo.fileexplorer.util.ToastUtils; 31 | 32 | import java.io.File; 33 | import java.util.ArrayList; 34 | import java.util.List; 35 | 36 | 37 | public class MainActivity extends BaseActivity { 38 | private long mLastPressBackTime = 0L; 39 | private String mPathCache = FileTools.ROOT_PATH; 40 | private File mDirectory; 41 | 42 | private NavController mNavController; 43 | 44 | private final IFileItemClickObserver mFileItemClickObserver = new IFileItemClickObserver() { 45 | @Override 46 | public void onClickDir(String path) { 47 | loadPath(path, true); 48 | } 49 | }; 50 | 51 | @Override 52 | protected void onCreate(@Nullable Bundle savedInstanceState) { 53 | super.onCreate(savedInstanceState); 54 | initView(); 55 | checkStoragePermission(); 56 | FileTools.addFileItemClickObserver(mFileItemClickObserver); 57 | } 58 | 59 | @Override 60 | protected void onResume() { 61 | super.onResume(); 62 | loadPath(mPathCache, false); 63 | } 64 | 65 | @Override 66 | protected void onDestroy() { 67 | super.onDestroy(); 68 | FileTools.removeFileItemClickObserver(mFileItemClickObserver); 69 | } 70 | 71 | private void initView() { 72 | NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fcv_file_list); 73 | if (navHostFragment != null) { 74 | mNavController = navHostFragment.getNavController(); 75 | } 76 | binding.btnBack.setOnClickListener(v -> { 77 | if (!FileTools.ROOT_PATH.equals(mDirectory.getPath())) { 78 | mDirectory = mDirectory.getParentFile(); 79 | assert mDirectory != null; 80 | mPathCache = mDirectory.getPath(); 81 | binding.tvPath.setText(mPathCache); 82 | mNavController.popBackStack(); 83 | } 84 | }); 85 | binding.tvPath.setOnLongClickListener(v -> { 86 | ClipboardManager manager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); 87 | manager.setPrimaryClip(new ClipData(new ClipDescription("", new String[] {ClipDescription.MIMETYPE_TEXT_PLAIN}), 88 | new ClipData.Item(binding.tvPath.getText()))); 89 | ToastUtils.shortCall(R.string.toast_path_copied_to_clip); 90 | return true; 91 | }); 92 | } 93 | 94 | private void loadPath(String path, boolean isUserClicked) { 95 | if (path == null) { 96 | return; 97 | } 98 | boolean isNavigate = !TextUtils.equals(mPathCache, path); 99 | mPathCache = path; 100 | if (FileTools.shouldRequestUriPermission(path)) { 101 | if (isUserClicked) { 102 | showRequestUriPermissionDialog(); 103 | } 104 | } else { 105 | mDirectory = new File(path); 106 | binding.tvPath.setText(mDirectory.getPath()); 107 | List list = FileTools.getSortedFileList(path); 108 | Bundle bundle = new Bundle(); 109 | bundle.putParcelableArrayList(BundleKey.FILE_LIST, (ArrayList) list); 110 | if (!isNavigate) { 111 | mNavController.popBackStack(); 112 | } 113 | mNavController.navigate(R.id.fileListFragment, bundle); 114 | } 115 | } 116 | 117 | @Override 118 | protected ActivityMainBinding onBinding() { 119 | return ActivityMainBinding.inflate(getLayoutInflater()); 120 | } 121 | 122 | private void showStoragePermissionDialog() { 123 | new AlertDialog.Builder(this) 124 | .setCancelable(false) 125 | .setMessage(R.string.dialog_storage_message) 126 | .setPositiveButton(R.string.dialog_button_request_permission, (dialog, which) -> { 127 | PermissionTools.requestStoragePermission(this); 128 | }) 129 | .setNegativeButton(R.string.dialog_button_cancel, (dialog, which) -> { 130 | finish(); 131 | }).create().show(); 132 | } 133 | 134 | private void showRequestUriPermissionDialog() { 135 | new AlertDialog.Builder(this) 136 | .setCancelable(false) 137 | .setMessage(R.string.dialog_need_uri_permission_message) 138 | .setPositiveButton(R.string.dialog_button_request_permission, (dialog, which) -> { 139 | FileTools.requestUriPermission(this, mPathCache); 140 | }) 141 | .setNegativeButton(R.string.dialog_button_cancel, (dialog, which) -> { 142 | }).create().show(); 143 | } 144 | 145 | private void checkStoragePermission() { 146 | if (PermissionTools.hasStoragePermission()) { 147 | loadPath(mPathCache, false); 148 | if (!SPUtils.getUseNewDocument()) { 149 | checkShizukuPermission(); 150 | } 151 | } else { 152 | showStoragePermissionDialog(); 153 | } 154 | } 155 | 156 | private void checkShizukuPermission() { 157 | // 安卓11以下不需要Shizuku,使用File接口就能浏览/sdcard全部文件 158 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 159 | if (PermissionTools.isShizukuAvailable()) { 160 | if (PermissionTools.hasShizukuPermission()) { 161 | FileTools.specialPathReadType = PathType.SHIZUKU; 162 | FileExplorerServiceManager.bindService(); 163 | } else { 164 | PermissionTools.requestShizukuPermission(); 165 | } 166 | } 167 | } 168 | } 169 | 170 | @Override 171 | protected void onStoragePermissionResult(boolean granted) { 172 | if (granted) { 173 | loadPath(mPathCache, false); 174 | checkShizukuPermission(); 175 | } else { 176 | showStoragePermissionDialog(); 177 | } 178 | } 179 | 180 | @Override 181 | protected void onDocumentPermissionResult(boolean granted) { 182 | if (granted) { 183 | loadPath(mPathCache, false); 184 | ToastUtils.shortCall(R.string.toast_permission_granted); 185 | } else { 186 | ToastUtils.shortCall(R.string.toast_permission_not_granted); 187 | } 188 | } 189 | 190 | @Override 191 | public void onBackPressed() { 192 | if (!FileTools.ROOT_PATH.equals(mDirectory.getPath())) { 193 | mDirectory = mDirectory.getParentFile(); 194 | assert mDirectory != null; 195 | mPathCache = mDirectory.getPath(); 196 | binding.tvPath.setText(mPathCache); 197 | super.onBackPressed(); 198 | } else { 199 | long time = System.currentTimeMillis(); 200 | if (time - mLastPressBackTime < 2000L) { 201 | super.onBackPressed(); 202 | finish(); 203 | } else { 204 | ToastUtils.shortCall(R.string.toast_press_back_again_to_exit); 205 | } 206 | mLastPressBackTime = time; 207 | } 208 | } 209 | 210 | @Override 211 | public boolean onOptionsItemSelected(@NonNull MenuItem item) { 212 | if (item.getItemId() == R.id.setting) { 213 | startActivity(new Intent(this, SettingActivity.class)); 214 | return true; 215 | } 216 | return super.onOptionsItemSelected(item); 217 | } 218 | 219 | @Override 220 | public boolean onCreateOptionsMenu(Menu menu) { 221 | getMenuInflater().inflate(R.menu.menu_main, menu); 222 | return true; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/activity/SettingActivity.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.activity; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.os.Bundle; 6 | import android.os.Process; 7 | import android.view.MotionEvent; 8 | 9 | import androidx.appcompat.app.AlertDialog; 10 | 11 | import com.magicianguo.fileexplorer.App; 12 | import com.magicianguo.fileexplorer.databinding.ActivitySettingBinding; 13 | import com.magicianguo.fileexplorer.util.SPUtils; 14 | 15 | public class SettingActivity extends BaseActivity { 16 | @Override 17 | @SuppressLint("ClickableViewAccessibility") 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | binding.swNewDocument.setChecked(SPUtils.getUseNewDocument()); 21 | binding.swNewDocument.setOnTouchListener((v, event) -> { 22 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 23 | v.setTag("clicked"); 24 | } 25 | return false; 26 | }); 27 | binding.swNewDocument.setOnCheckedChangeListener((buttonView, isChecked) -> { 28 | if (!"clicked".equals(buttonView.getTag())) { 29 | return; 30 | } 31 | buttonView.setTag(""); 32 | new AlertDialog.Builder(this) 33 | .setMessage("切换后需要重新打开文件管理") 34 | .setPositiveButton("确定", (dialog, which) -> { 35 | buttonView.setChecked(isChecked); 36 | SPUtils.setUseNewDocument(isChecked); 37 | buttonView.postDelayed(() -> { 38 | for (Activity activity : App.getAliveActivityList()) { 39 | activity.finish(); 40 | } 41 | Process.killProcess(Process.myPid()); 42 | }, 200L); 43 | }) 44 | .setNegativeButton("取消", (dialog, which) -> { 45 | buttonView.setChecked(!isChecked); 46 | }) 47 | .show(); 48 | }); 49 | } 50 | 51 | @Override 52 | protected ActivitySettingBinding onBinding() { 53 | return ActivitySettingBinding.inflate(getLayoutInflater()); 54 | } 55 | } -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/adapter/FileListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.adapter; 2 | 3 | import android.content.pm.PackageManager; 4 | import android.graphics.Color; 5 | import android.graphics.drawable.Drawable; 6 | import android.view.LayoutInflater; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.recyclerview.widget.RecyclerView; 13 | 14 | import com.magicianguo.fileexplorer.App; 15 | import com.magicianguo.fileexplorer.R; 16 | import com.magicianguo.fileexplorer.bean.BeanFile; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | public class FileListAdapter extends RecyclerView.Adapter { 22 | private final List mList = new ArrayList<>(); 23 | private IItemClickListener mListener; 24 | 25 | private final PackageManager PACKAGE_MANAGER = App.get().getPackageManager(); 26 | 27 | public void setListener(IItemClickListener listener) { 28 | mListener = listener; 29 | } 30 | 31 | public void updateList(List list) { 32 | mList.clear(); 33 | mList.addAll(list); 34 | notifyDataSetChanged(); 35 | } 36 | 37 | 38 | @NonNull 39 | @Override 40 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 41 | return new FileListHolder(parent); 42 | } 43 | 44 | @Override 45 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { 46 | if (holder instanceof FileListHolder) { 47 | FileListHolder fileListHolder = (FileListHolder) holder; 48 | BeanFile file = mList.get(position); 49 | fileListHolder.tvName.setText(file.name); 50 | if (file.isGrantedPath) { 51 | fileListHolder.tvName.setTextColor(Color.BLUE); 52 | } else { 53 | fileListHolder.tvName.setTextColor(Color.BLACK); 54 | } 55 | if (file.isDir) { 56 | fileListHolder.tvIcon.setText(R.string.file_item_icon_txt_dir); 57 | fileListHolder.tvIcon.setBackgroundResource(R.drawable.bg_icon_dir); 58 | } else { 59 | fileListHolder.tvIcon.setText(R.string.file_item_icon_txt_file); 60 | fileListHolder.tvIcon.setBackgroundResource(R.drawable.bg_icon_file); 61 | } 62 | if (file.pathPackageName != null) { 63 | fileListHolder.ivSmall.setImageDrawable(getAppIconDrawable(file)); 64 | } else { 65 | fileListHolder.ivSmall.setImageDrawable(null); 66 | } 67 | fileListHolder.itemView.setOnClickListener(v -> { 68 | if (mListener != null && file.isDir) { 69 | mListener.onClickDir(file.path); 70 | } 71 | }); 72 | } 73 | } 74 | 75 | @Override 76 | public int getItemCount() { 77 | return mList.size(); 78 | } 79 | 80 | private Drawable getAppIconDrawable(BeanFile file) { 81 | try { 82 | return PACKAGE_MANAGER.getApplicationIcon(file.pathPackageName); 83 | } catch (PackageManager.NameNotFoundException e) { 84 | e.printStackTrace(); 85 | } 86 | return null; 87 | } 88 | 89 | private static class FileListHolder extends RecyclerView.ViewHolder { 90 | TextView tvIcon = itemView.findViewById(R.id.tv_icon); 91 | ImageView ivSmall = itemView.findViewById(R.id.iv_small_icon); 92 | TextView tvName = itemView.findViewById(R.id.tv_name); 93 | 94 | public FileListHolder(ViewGroup parent) { 95 | super(LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_file_list_item, parent, false)); 96 | } 97 | } 98 | 99 | public interface IItemClickListener { 100 | void onClickDir(String path); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/bean/BeanFile.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.bean; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | public class BeanFile implements Parcelable { 9 | public BeanFile(String name, String path, boolean isDir, boolean isGrantedPath, String pathPackageName) { 10 | this.name = name; 11 | this.path = path; 12 | this.isDir = isDir; 13 | this.isGrantedPath = isGrantedPath; 14 | this.pathPackageName = pathPackageName; 15 | } 16 | 17 | /** 18 | * 文件名 19 | */ 20 | public String name; 21 | /** 22 | * 文件路径 23 | */ 24 | public String path; 25 | /** 26 | * 是否文件夹 27 | */ 28 | public boolean isDir; 29 | /** 30 | * 是否被Document授权的路径 31 | */ 32 | public boolean isGrantedPath; 33 | /** 34 | * 如果文件夹名称是应用包名,则将包名保存到该字段 35 | */ 36 | public String pathPackageName; 37 | 38 | protected BeanFile(Parcel in) { 39 | name = in.readString(); 40 | path = in.readString(); 41 | isDir = in.readByte() != 0; 42 | isGrantedPath = in.readByte() != 0; 43 | pathPackageName = in.readString(); 44 | } 45 | 46 | public static final Creator CREATOR = new Creator() { 47 | @Override 48 | public BeanFile createFromParcel(Parcel in) { 49 | return new BeanFile(in); 50 | } 51 | 52 | @Override 53 | public BeanFile[] newArray(int size) { 54 | return new BeanFile[size]; 55 | } 56 | }; 57 | 58 | @Override 59 | public int describeContents() { 60 | return 0; 61 | } 62 | 63 | @Override 64 | public void writeToParcel(@NonNull Parcel dest, int flags) { 65 | dest.writeString(name); 66 | dest.writeString(path); 67 | dest.writeByte((byte) (isDir ? 1 : 0)); 68 | dest.writeByte((byte) (isGrantedPath ? 1 : 0)); 69 | dest.writeString(pathPackageName); 70 | } 71 | 72 | @NonNull 73 | @Override 74 | public String toString() { 75 | return "BeanFile{" + 76 | "name='" + name + '\'' + 77 | ", path='" + path + '\'' + 78 | ", isDir=" + isDir + 79 | ", isGrantedPath=" + isGrantedPath + 80 | ", pathPackageName='" + pathPackageName + '\'' + 81 | '}'; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/constant/BundleKey.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.constant; 2 | 3 | public interface BundleKey { 4 | String FILE_LIST = "FILE_LIST"; 5 | } 6 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/constant/PathType.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.constant; 2 | 3 | import androidx.annotation.IntDef; 4 | 5 | public interface PathType { 6 | /** 7 | * 通过File接口,访问一般路径 8 | */ 9 | int FILE = 0; 10 | /** 11 | * 通过Document API 访问特殊路径 12 | */ 13 | int DOCUMENT = 1; 14 | /** 15 | * 安卓13及以上,直接用包名展示data、obb下的目录(因为data、obb不能直接授权了,只能对子目录授权) 16 | */ 17 | int PACKAGE_NAME = 2; 18 | /** 19 | * 通过Shizuku授权访问特殊路径 20 | */ 21 | int SHIZUKU = 3; 22 | 23 | @IntDef({ FILE, DOCUMENT, PACKAGE_NAME, SHIZUKU }) 24 | @interface PathType1 { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/constant/RequestCode.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.constant; 2 | 3 | public interface RequestCode { 4 | int STORAGE = 0; 5 | int DOCUMENT = 1; 6 | int SHIZUKU = 2; 7 | } 8 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/fragment/EmptyFragment.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.fragment; 2 | 3 | import androidx.fragment.app.Fragment; 4 | 5 | public class EmptyFragment extends Fragment { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/fragment/FileListFragment.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.os.Parcelable; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.fragment.app.Fragment; 12 | import androidx.recyclerview.widget.LinearLayoutManager; 13 | import androidx.recyclerview.widget.RecyclerView; 14 | 15 | import com.magicianguo.fileexplorer.R; 16 | import com.magicianguo.fileexplorer.adapter.FileListAdapter; 17 | import com.magicianguo.fileexplorer.bean.BeanFile; 18 | import com.magicianguo.fileexplorer.constant.BundleKey; 19 | import com.magicianguo.fileexplorer.util.FileTools; 20 | 21 | import java.util.ArrayList; 22 | 23 | public class FileListFragment extends Fragment { 24 | private final FileListAdapter mAdapter = new FileListAdapter(); 25 | 26 | @Nullable 27 | @Override 28 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 29 | return inflater.inflate(R.layout.fragment_file_list, container, false); 30 | } 31 | 32 | @Override 33 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 34 | RecyclerView rvList = view.findViewById(R.id.rv_files); 35 | mAdapter.setListener(new FileListAdapter.IItemClickListener() { 36 | @Override 37 | public void onClickDir(String path) { 38 | FileTools.notifyClickDir(path); 39 | } 40 | }); 41 | rvList.setLayoutManager(new LinearLayoutManager(requireContext())); 42 | rvList.setAdapter(mAdapter); 43 | Bundle arguments = getArguments(); 44 | if (arguments != null) { 45 | ArrayList list = arguments.getParcelableArrayList(BundleKey.FILE_LIST); 46 | mAdapter.updateList(list); 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/observer/IFileItemClickObserver.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.observer; 2 | 3 | public interface IFileItemClickObserver { 4 | void onClickDir(String path); 5 | } 6 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/userservice/FileExplorerService.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.userservice; 2 | 3 | import android.os.RemoteException; 4 | import com.magicianguo.fileexplorer.bean.BeanFile; 5 | 6 | import java.io.File; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class FileExplorerService extends IFileExplorerService.Stub { 11 | private static final String TAG = "FileExplorerService"; 12 | 13 | @Override 14 | public List listFiles(String path) throws RemoteException { 15 | List list = new ArrayList<>(); 16 | File[] files = new File(path).listFiles(); 17 | if (files != null) { 18 | for (File f : files) { 19 | list.add(new BeanFile(f.getName(), f.getPath(), f.isDirectory(), false, f.getName())); 20 | } 21 | } 22 | return list; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/userservice/FileExplorerServiceManager.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.userservice; 2 | 3 | import android.content.ComponentName; 4 | import android.content.ServiceConnection; 5 | import android.os.IBinder; 6 | import android.util.Log; 7 | 8 | import com.magicianguo.fileexplorer.App; 9 | import com.magicianguo.fileexplorer.BuildConfig; 10 | import com.magicianguo.fileexplorer.R; 11 | import com.magicianguo.fileexplorer.util.FileTools; 12 | import com.magicianguo.fileexplorer.util.ToastUtils; 13 | 14 | import rikka.shizuku.Shizuku; 15 | 16 | public class FileExplorerServiceManager { 17 | private static final String TAG = "FileExplorerServiceManager"; 18 | private static boolean isBind = false; 19 | 20 | private static final Shizuku.UserServiceArgs USER_SERVICE_ARGS = new Shizuku.UserServiceArgs( 21 | new ComponentName(App.get().getPackageName(), FileExplorerService.class.getName()) 22 | ).daemon(false).debuggable(BuildConfig.DEBUG).processNameSuffix("file_explorer_service").version(1); 23 | 24 | private static final ServiceConnection SERVICE_CONNECTION = new ServiceConnection() { 25 | @Override 26 | public void onServiceConnected(ComponentName name, IBinder service) { 27 | Log.d(TAG, "onServiceConnected: "); 28 | isBind = true; 29 | FileTools.iFileExplorerService = IFileExplorerService.Stub.asInterface(service); 30 | ToastUtils.shortCall(R.string.toast_shizuku_connected); 31 | } 32 | 33 | @Override 34 | public void onServiceDisconnected(ComponentName name) { 35 | Log.d(TAG, "onServiceDisconnected: "); 36 | isBind = false; 37 | FileTools.iFileExplorerService = null; 38 | ToastUtils.shortCall(R.string.toast_shizuku_disconnected); 39 | } 40 | }; 41 | 42 | public static void bindService() { 43 | Log.d(TAG, "bindService: isBind = " + isBind); 44 | if (!isBind) { 45 | Shizuku.bindUserService(USER_SERVICE_ARGS, SERVICE_CONNECTION); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/util/FileTools.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.content.UriPermission; 6 | import android.content.pm.PackageInfo; 7 | import android.content.pm.PackageManager; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Environment; 11 | import android.os.RemoteException; 12 | import android.provider.DocumentsContract; 13 | import android.util.Log; 14 | 15 | import androidx.documentfile.provider.DocumentFile; 16 | 17 | import com.magicianguo.fileexplorer.App; 18 | import com.magicianguo.fileexplorer.R; 19 | import com.magicianguo.fileexplorer.bean.BeanFile; 20 | import com.magicianguo.fileexplorer.constant.PathType; 21 | import com.magicianguo.fileexplorer.constant.RequestCode; 22 | import com.magicianguo.fileexplorer.observer.IFileItemClickObserver; 23 | import com.magicianguo.fileexplorer.userservice.IFileExplorerService; 24 | 25 | import java.io.File; 26 | import java.util.ArrayList; 27 | import java.util.Comparator; 28 | import java.util.List; 29 | 30 | import kotlin.collections.CollectionsKt; 31 | 32 | public class FileTools { 33 | public static final String ROOT_PATH = Environment.getExternalStorageDirectory().getPath(); 34 | public static int specialPathReadType = PathType.DOCUMENT; 35 | 36 | private static final PackageManager PACKAGE_MANAGER = App.get().getPackageManager(); 37 | 38 | private static final Comparator COMPARATOR = new Comparator() { 39 | @Override 40 | public int compare(BeanFile o1, BeanFile o2) { 41 | String name1 = o1.name; 42 | String name2 = o2.name; 43 | int compareCount = Math.max(name1.length(), name2.length()); 44 | for (int i = 0; i < compareCount; i++) { 45 | int code1 = getCharCode(name1, i); 46 | int code2 = getCharCode(name2, i); 47 | if (code1 != code2) { 48 | return code1 - code2; 49 | } 50 | } 51 | return 0; 52 | } 53 | 54 | private int getCharCode(String str, int index) { 55 | if (index >= str.length()) { 56 | return -1; 57 | } 58 | char c = str.charAt(index); 59 | if (Character.isLetter(c)) { 60 | if (Character.isLowerCase(c)) { 61 | return Character.toUpperCase(c); 62 | } else { 63 | return c; 64 | } 65 | } else { 66 | return -1; 67 | } 68 | } 69 | }; 70 | 71 | public static IFileExplorerService iFileExplorerService; 72 | 73 | public static List getSortedFileList(String path) { 74 | List fileList = getFileList(path); 75 | CollectionsKt.sortWith(fileList, COMPARATOR); 76 | return fileList; 77 | } 78 | 79 | private static List getFileList(String path) { 80 | int type = getPathType(path); 81 | if (type == PathType.SHIZUKU) { 82 | return getFileListByShizuku(path); 83 | } 84 | if (type == PathType.DOCUMENT) { 85 | return getFileListByDocument(path); 86 | } 87 | if (type == PathType.PACKAGE_NAME) { 88 | return getPackageNameFileList(path); 89 | } 90 | return getFileListByFile(path); 91 | } 92 | 93 | private static List getFileListByFile(String path) { 94 | boolean isPkgNamePath = isDataPath(path) || isObbPath(path); 95 | List list = new ArrayList<>(); 96 | File dir = new File(path); 97 | File[] files; 98 | if ((files = dir.listFiles()) != null) { 99 | for (File file : files) { 100 | list.add(new BeanFile(file.getName(), file.getPath(), file.isDirectory(), false, isPkgNamePath ? file.getName() : null)); 101 | } 102 | } 103 | return list; 104 | } 105 | 106 | private static List getFileListByDocument(String path) { 107 | Uri pathUri = pathToUri(path); 108 | Log.d("TAG", "getFileListByDocument: pathUri = "+pathUri); 109 | DocumentFile documentFile = DocumentFile.fromTreeUri(App.get(), pathUri); 110 | List list = new ArrayList<>(); 111 | if (documentFile != null) { 112 | DocumentFile[] documentFiles = documentFile.listFiles(); 113 | for (DocumentFile df : documentFiles) { 114 | String fName = df.getName(); 115 | String fPath = path + "/" + fName; 116 | list.add(new BeanFile(fName, fPath, df.isDirectory(), false, getPathPackageName(fName))); 117 | } 118 | } 119 | return list; 120 | } 121 | 122 | private static List getFileListByShizuku(String path) { 123 | try { 124 | return iFileExplorerService.listFiles(path); 125 | } catch (NullPointerException | RemoteException e) { 126 | ToastUtils.longCall(R.string.toast_shizuku_load_file_failed); 127 | e.printStackTrace(); 128 | } 129 | return new ArrayList<>(); 130 | } 131 | 132 | private static List getPackageNameFileList(String path) { 133 | List list = new ArrayList<>(); 134 | List installedPackages = PACKAGE_MANAGER.getInstalledPackages(0); 135 | for (PackageInfo packageInfo : installedPackages) { 136 | String packageName = packageInfo.packageName; 137 | String dirPath = path + "/" + packageName; 138 | File dir = new File(dirPath); 139 | if (dir.exists()) { 140 | list.add(new BeanFile(packageName, dirPath, true, hasUriPermission(dirPath) || isFromMyPackageNamePath(dirPath), packageName)); 141 | } 142 | } 143 | return list; 144 | } 145 | 146 | public static boolean shouldRequestUriPermission(String path) { 147 | if (getPathType(path) != PathType.DOCUMENT) { 148 | return false; 149 | } 150 | return !hasUriPermission(path); 151 | } 152 | 153 | @PathType.PathType1 154 | private static int getPathType(String path) { 155 | if (isFromMyPackageNamePath(path)) { 156 | return PathType.FILE; 157 | } 158 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !SPUtils.getUseNewDocument()) { 159 | if (isDataPath(path) || isObbPath(path)) { 160 | if (specialPathReadType == PathType.SHIZUKU) { 161 | return PathType.SHIZUKU; 162 | } else { 163 | return PathType.PACKAGE_NAME; 164 | } 165 | } else if (isUnderDataPath(path) || isUnderObbPath(path)) { 166 | if (specialPathReadType == PathType.SHIZUKU) { 167 | return PathType.SHIZUKU; 168 | } else { 169 | return PathType.DOCUMENT; 170 | } 171 | } else { 172 | return PathType.FILE; 173 | } 174 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 175 | if (isDataPath(path) || isObbPath(path) || isUnderDataPath(path) || isUnderObbPath(path)) { 176 | if (specialPathReadType == PathType.SHIZUKU) { 177 | return PathType.SHIZUKU; 178 | } else { 179 | return PathType.DOCUMENT; 180 | } 181 | } else { 182 | return PathType.FILE; 183 | } 184 | } else { 185 | return PathType.FILE; 186 | } 187 | } 188 | 189 | private static boolean hasUriPermission(String path) { 190 | List uriPermissions = App.get().getContentResolver().getPersistedUriPermissions(); 191 | Log.d("TAG", "hasUriPermission: uriPermissions = " + uriPermissions); 192 | String uriPath = pathToUri(path).getPath(); 193 | Log.d("TAG", "hasUriPermission: uriPath = "+uriPath); 194 | for (UriPermission uriPermission : uriPermissions) { 195 | String itemPath = uriPermission.getUri().getPath(); 196 | Log.d("TAG", "hasUriPermission: itemPath = " + itemPath); 197 | if (uriPath != null && itemPath != null && (uriPath + "/").contains(itemPath + "/")) { 198 | return true; 199 | } 200 | } 201 | return false; 202 | } 203 | 204 | public static void requestUriPermission(Activity activity, String path) { 205 | Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); 206 | intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | 207 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 208 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 209 | Uri treeUri = pathToUri(path); 210 | DocumentFile df = DocumentFile.fromTreeUri(activity, treeUri); 211 | if (df != null) { 212 | intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, df.getUri()); 213 | } 214 | } 215 | activity.startActivityForResult(intent, RequestCode.DOCUMENT); 216 | } 217 | 218 | private static Uri pathToUri(String path) { 219 | String halfPath = path.replace(ROOT_PATH + "/", ""); 220 | String[] segments = halfPath.split("/"); 221 | Uri.Builder uriBuilder = new Uri.Builder() 222 | .scheme("content") 223 | .authority("com.android.externalstorage.documents") 224 | .appendPath("tree"); 225 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 226 | if (SPUtils.getUseNewDocument()) { 227 | uriBuilder.appendPath("primary:A\u200Bndroid/" + segments[1]); 228 | } else { 229 | uriBuilder.appendPath("primary:Android/" + segments[1] + "/" + segments[2]); 230 | } 231 | } else { 232 | uriBuilder.appendPath("primary:Android/" + segments[1]); 233 | } 234 | uriBuilder.appendPath("document"); 235 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && SPUtils.getUseNewDocument()) { 236 | uriBuilder.appendPath("primary:A\u200Bndroid/" + halfPath.replace("Android/", "")); 237 | } else { 238 | uriBuilder.appendPath("primary:" + halfPath); 239 | } 240 | return uriBuilder.build(); 241 | } 242 | 243 | private static boolean isFromMyPackageNamePath(String path) { 244 | return (path + "/").contains(ROOT_PATH + "/Android/data/"+ App.get().getPackageName()+"/"); 245 | } 246 | 247 | private static boolean isDataPath(String path) { 248 | return (ROOT_PATH + "/Android/data").equals(path); 249 | } 250 | 251 | private static boolean isObbPath(String path) { 252 | return (ROOT_PATH + "/Android/obb").equals(path); 253 | } 254 | 255 | private static boolean isUnderDataPath(String path) { 256 | return path.contains(ROOT_PATH + "/Android/data/"); 257 | } 258 | 259 | private static boolean isUnderObbPath(String path) { 260 | return path.contains(ROOT_PATH + "/Android/obb/"); 261 | } 262 | 263 | /** 264 | * 如果字符串是应用包名,返回字符串,反之返回null 265 | */ 266 | public static String getPathPackageName(String name) { 267 | try { 268 | PACKAGE_MANAGER.getPackageInfo(name, 0); 269 | return name; 270 | } catch (PackageManager.NameNotFoundException ignore) { 271 | } 272 | return null; 273 | } 274 | 275 | private static List sFileItemClickObservers = new ArrayList<>(); 276 | 277 | public static void addFileItemClickObserver(IFileItemClickObserver observer) { 278 | sFileItemClickObservers.add(observer); 279 | } 280 | 281 | public static void removeFileItemClickObserver(IFileItemClickObserver observer) { 282 | sFileItemClickObservers.remove(observer); 283 | } 284 | 285 | public static void notifyClickDir(String path) { 286 | for (IFileItemClickObserver observer : sFileItemClickObservers) { 287 | observer.onClickDir(path); 288 | } 289 | } 290 | 291 | } 292 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/util/PermissionTools.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.util; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.pm.PackageManager; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Environment; 11 | import android.provider.Settings; 12 | 13 | import com.magicianguo.fileexplorer.App; 14 | import com.magicianguo.fileexplorer.constant.RequestCode; 15 | 16 | import rikka.shizuku.Shizuku; 17 | 18 | public class PermissionTools { 19 | private static final String SHIZUKU_PACKAGE_NAME = "moe.shizuku.privileged.api"; 20 | 21 | public static boolean hasStoragePermission() { 22 | Context context = App.get(); 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 24 | return Environment.isExternalStorageManager(); 25 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 26 | return context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == 27 | PackageManager.PERMISSION_GRANTED; 28 | } else { 29 | return true; 30 | } 31 | } 32 | 33 | public static void requestStoragePermission(Activity activity) { 34 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 35 | Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) 36 | .setData(Uri.parse("package:"+activity.getPackageName())); 37 | activity.startActivityForResult(intent, RequestCode.STORAGE); 38 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 39 | activity.requestPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE } , RequestCode.STORAGE); 40 | } 41 | } 42 | 43 | private static boolean isShizukuInstalled() { 44 | try { 45 | App.get().getPackageManager().getPackageInfo(SHIZUKU_PACKAGE_NAME, 0); 46 | return true; 47 | } catch (PackageManager.NameNotFoundException e) { 48 | e.printStackTrace(); 49 | } 50 | return false; 51 | } 52 | 53 | public static boolean isShizukuAvailable() { 54 | return isShizukuInstalled() && Shizuku.pingBinder(); 55 | } 56 | 57 | public static boolean hasShizukuPermission() { 58 | return Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED; 59 | } 60 | 61 | /** 62 | * 请求权限。 63 | * @return Shizuku是否可用 64 | */ 65 | public static void requestShizukuPermission() { 66 | Shizuku.requestPermission(RequestCode.SHIZUKU); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/util/SPUtils.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.util; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.magicianguo.fileexplorer.App; 7 | 8 | public class SPUtils { 9 | private static final SharedPreferences SP = App.get().getSharedPreferences("SPUtils", Context.MODE_PRIVATE); 10 | private static final String KEY_USE_NEW_DOCUMENT = "use_new_document"; 11 | 12 | public static void setUseNewDocument(boolean use) { 13 | SP.edit().putBoolean(KEY_USE_NEW_DOCUMENT, use).apply(); 14 | } 15 | 16 | public static boolean getUseNewDocument() { 17 | return SP.getBoolean(KEY_USE_NEW_DOCUMENT, true); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /FileExplorer/src/main/java/com/magicianguo/fileexplorer/util/ToastUtils.java: -------------------------------------------------------------------------------- 1 | package com.magicianguo.fileexplorer.util; 2 | 3 | import android.widget.Toast; 4 | 5 | import androidx.annotation.StringRes; 6 | 7 | import com.magicianguo.fileexplorer.App; 8 | 9 | public class ToastUtils { 10 | private static Toast sToast; 11 | 12 | public static void shortCall(@StringRes int resId) { 13 | shortCall(App.get().getString(resId)); 14 | } 15 | 16 | public static void shortCall(String text) { 17 | cancelToast(); 18 | sToast = Toast.makeText(App.get(), text, Toast.LENGTH_SHORT); 19 | sToast.show(); 20 | } 21 | 22 | public static void longCall(@StringRes int resId) { 23 | longCall(App.get().getString(resId)); 24 | } 25 | 26 | public static void longCall(String text) { 27 | cancelToast(); 28 | sToast = Toast.makeText(App.get(), text, Toast.LENGTH_LONG); 29 | sToast.show(); 30 | } 31 | 32 | private static void cancelToast() { 33 | if (sToast != null) { 34 | sToast.cancel(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /FileExplorer/src/main/res/drawable/bg_file_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /FileExplorer/src/main/res/drawable/bg_icon_dir.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /FileExplorer/src/main/res/drawable/bg_icon_file.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /FileExplorer/src/main/res/drawable/bg_tv_path.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /FileExplorer/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 28 | 29 |