feat:add so manager

This commit is contained in:
jiqiu2021
2025-06-25 18:27:51 +08:00
parent 7d8b86f374
commit 5632194bda
10 changed files with 854 additions and 98 deletions

View File

@@ -20,6 +20,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".FileBrowserActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".AppSoConfigActivity"
android:parentActivityName=".MainActivity" />
</application>
</manifest>

View File

@@ -0,0 +1,164 @@
package com.jiqiu.configapp;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList;
import java.util.List;
public class AppSoConfigActivity extends AppCompatActivity {
public static final String EXTRA_PACKAGE_NAME = "package_name";
public static final String EXTRA_APP_NAME = "app_name";
private RecyclerView recyclerView;
private TextView emptyView;
private SoSelectionAdapter adapter;
private ConfigManager configManager;
private String packageName;
private String appName;
private List<ConfigManager.SoFile> appSoFiles;
private List<ConfigManager.SoFile> globalSoFiles;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_so_config);
packageName = getIntent().getStringExtra(EXTRA_PACKAGE_NAME);
appName = getIntent().getStringExtra(EXTRA_APP_NAME);
if (packageName == null) {
finish();
return;
}
configManager = new ConfigManager(this);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(appName != null ? appName : packageName);
getSupportActionBar().setSubtitle("配置SO文件");
recyclerView = findViewById(R.id.recyclerView);
emptyView = findViewById(R.id.emptyView);
adapter = new SoSelectionAdapter();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
loadData();
}
private void loadData() {
// Load app-specific SO files
appSoFiles = configManager.getAppSoFiles(packageName);
// Load global SO files
globalSoFiles = configManager.getAllSoFiles();
if (globalSoFiles.isEmpty()) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
adapter.setData(globalSoFiles, appSoFiles);
}
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
class SoSelectionAdapter extends RecyclerView.Adapter<SoSelectionAdapter.ViewHolder> {
private List<ConfigManager.SoFile> availableSoFiles = new ArrayList<>();
private List<ConfigManager.SoFile> selectedSoFiles = new ArrayList<>();
void setData(List<ConfigManager.SoFile> available, List<ConfigManager.SoFile> selected) {
this.availableSoFiles = available;
this.selectedSoFiles = selected;
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_so_selection, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(availableSoFiles.get(position));
}
@Override
public int getItemCount() {
return availableSoFiles.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
CheckBox checkBox;
TextView nameText;
TextView pathText;
ViewHolder(@NonNull View itemView) {
super(itemView);
checkBox = itemView.findViewById(R.id.checkBox);
nameText = itemView.findViewById(R.id.textName);
pathText = itemView.findViewById(R.id.textPath);
}
void bind(ConfigManager.SoFile soFile) {
nameText.setText(soFile.name);
pathText.setText(soFile.originalPath);
// Check if this SO is selected for the app
boolean isSelected = false;
for (ConfigManager.SoFile selected : selectedSoFiles) {
if (selected.storedPath.equals(soFile.storedPath)) {
isSelected = true;
break;
}
}
checkBox.setOnCheckedChangeListener(null);
checkBox.setChecked(isSelected);
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
// Add SO to app
configManager.addSoFileToApp(packageName, soFile.originalPath, false);
} else {
// Remove SO from app
configManager.removeSoFileFromApp(packageName, soFile);
}
// Reload data
loadData();
});
itemView.setOnClickListener(v -> checkBox.toggle());
}
}
}
}

View File

@@ -104,6 +104,48 @@ public class ConfigManager {
return new ArrayList<>(appConfig.soFiles);
}
public List<SoFile> getAllSoFiles() {
if (config.globalSoFiles == null) {
config.globalSoFiles = new ArrayList<>();
}
return new ArrayList<>(config.globalSoFiles);
}
public void addGlobalSoFile(String originalPath, boolean deleteOriginal) {
if (config.globalSoFiles == null) {
config.globalSoFiles = new ArrayList<>();
}
// Generate unique filename
String fileName = new File(originalPath).getName();
String storedPath = SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
// Copy SO file to our storage
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
if (result.isSuccess()) {
SoFile soFile = new SoFile();
soFile.name = fileName;
soFile.storedPath = storedPath;
soFile.originalPath = originalPath;
config.globalSoFiles.add(soFile);
if (deleteOriginal) {
Shell.cmd("rm \"" + originalPath + "\"").exec();
}
saveConfig();
}
}
public void removeGlobalSoFile(SoFile soFile) {
if (config.globalSoFiles == null) return;
config.globalSoFiles.remove(soFile);
// Delete the stored file
Shell.cmd("rm \"" + soFile.storedPath + "\"").exec();
saveConfig();
}
public void addSoFileToApp(String packageName, String originalPath, boolean deleteOriginal) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
@@ -116,7 +158,7 @@ public class ConfigManager {
String storedPath = SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
// Copy SO file to our storage
Shell.Result result = Shell.cmd("cp " + originalPath + " " + storedPath).exec();
Shell.Result result = Shell.cmd("cp \"" + originalPath + "\" \"" + storedPath + "\"").exec();
if (result.isSuccess()) {
SoFile soFile = new SoFile();
soFile.name = fileName;
@@ -125,7 +167,7 @@ public class ConfigManager {
appConfig.soFiles.add(soFile);
if (deleteOriginal) {
Shell.cmd("rm " + originalPath).exec();
Shell.cmd("rm \"" + originalPath + "\"").exec();
}
saveConfig();
@@ -155,6 +197,7 @@ public class ConfigManager {
public static class ModuleConfig {
public boolean enabled = true;
public boolean hideInjection = false;
public List<SoFile> globalSoFiles = new ArrayList<>();
public Map<String, AppConfig> perAppConfig = new HashMap<>();
}

View File

@@ -0,0 +1,287 @@
package com.jiqiu.configapp;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.topjohnwu.superuser.Shell;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FileBrowserActivity extends AppCompatActivity {
private static final String TAG = "FileBrowser";
public static final String EXTRA_START_PATH = "start_path";
public static final String EXTRA_FILE_FILTER = "file_filter";
public static final String EXTRA_SELECTED_PATH = "selected_path";
private RecyclerView recyclerView;
private TextView currentPathText;
private View emptyView;
private FileListAdapter adapter;
private String currentPath;
private String fileFilter = ".so";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_file_browser);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle("选择SO文件");
currentPathText = findViewById(R.id.currentPath);
recyclerView = findViewById(R.id.recyclerView);
emptyView = findViewById(R.id.emptyView);
adapter = new FileListAdapter();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setAdapter(adapter);
// Get start path from intent
String startPath = getIntent().getStringExtra(EXTRA_START_PATH);
if (startPath == null) {
startPath = "/data/local/tmp";
}
fileFilter = getIntent().getStringExtra(EXTRA_FILE_FILTER);
if (fileFilter == null) {
fileFilter = ".so";
}
// Check if we have root access
if (!Shell.getShell().isRoot()) {
Toast.makeText(this, "需要Root权限才能浏览文件", Toast.LENGTH_LONG).show();
Log.e(TAG, "No root access");
}
currentPath = startPath;
loadFiles();
}
private void loadFiles() {
currentPathText.setText(currentPath);
List<FileItem> items = new ArrayList<>();
// Add parent directory if not root
if (!"/".equals(currentPath)) {
items.add(new FileItem("..", true, true));
}
// List files using root
Log.d(TAG, "Loading files from: " + currentPath);
Shell.Result result = Shell.cmd("ls -la " + currentPath + " 2>/dev/null").exec();
Log.d(TAG, "ls command success: " + result.isSuccess() + ", output lines: " + result.getOut().size());
if (result.isSuccess()) {
for (String line : result.getOut()) {
// Skip empty lines, total line, and symbolic links
if (line.trim().isEmpty() || line.startsWith("total") || line.contains("->")) {
continue;
}
// Try to parse ls output - handle different formats
String name = null;
boolean isDirectory = false;
boolean isReadable = true;
// Check if line starts with permissions (drwxr-xr-x format)
if (line.matches("^[dlrwxst-]{10}.*")) {
String[] parts = line.split("\\s+", 9);
if (parts.length >= 9) {
String permissions = parts[0];
name = parts[parts.length - 1];
isDirectory = permissions.startsWith("d");
isReadable = permissions.length() > 1 && permissions.charAt(1) == 'r';
}
} else {
// Simple format, just the filename
name = line.trim();
// Check if it's a directory by trying to list it
Shell.Result dirCheck = Shell.cmd("test -d \"" + currentPath + "/" + name + "\" && echo 'dir'").exec();
isDirectory = dirCheck.isSuccess() && !dirCheck.getOut().isEmpty();
}
if (name != null && !".".equals(name) && !"..".equals(name)) {
// Filter files by extension
if (!isDirectory && fileFilter != null && !name.endsWith(fileFilter)) {
continue;
}
items.add(new FileItem(name, isDirectory, isReadable));
}
}
} else {
// If ls fails, try a simpler approach
Shell.Result simpleResult = Shell.cmd("cd " + currentPath + " && for f in *; do echo \"$f\"; done").exec();
if (simpleResult.isSuccess()) {
for (String name : simpleResult.getOut()) {
if (!name.trim().isEmpty() && !"*".equals(name)) {
Shell.Result dirCheck = Shell.cmd("test -d \"" + currentPath + "/" + name + "\" && echo 'dir'").exec();
boolean isDirectory = dirCheck.isSuccess() && !dirCheck.getOut().isEmpty();
// Filter files by extension
if (!isDirectory && fileFilter != null && !name.endsWith(fileFilter)) {
continue;
}
items.add(new FileItem(name, isDirectory, true));
}
}
}
}
// If still no items and not root, add some common directories to try
if (items.size() <= 1 && "/data/local/tmp".equals(currentPath)) {
// Try to create a test file to verify access
Shell.cmd("touch /data/local/tmp/test_access.tmp && rm /data/local/tmp/test_access.tmp").exec();
// Add any .so files we can find
Shell.Result findResult = Shell.cmd("find " + currentPath + " -maxdepth 1 -name '*.so' -type f 2>/dev/null").exec();
if (findResult.isSuccess()) {
for (String path : findResult.getOut()) {
if (!path.trim().isEmpty()) {
String name = path.substring(path.lastIndexOf('/') + 1);
items.add(new FileItem(name, false, true));
}
}
}
}
Collections.sort(items, (a, b) -> {
if (a.isDirectory != b.isDirectory) {
return a.isDirectory ? -1 : 1;
}
return a.name.compareToIgnoreCase(b.name);
});
adapter.setItems(items);
emptyView.setVisibility(items.isEmpty() || (items.size() == 1 && "..".equals(items.get(0).name)) ? View.VISIBLE : View.GONE);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
class FileItem {
String name;
boolean isDirectory;
boolean isReadable;
FileItem(String name, boolean isDirectory, boolean isReadable) {
this.name = name;
this.isDirectory = isDirectory;
this.isReadable = isReadable;
}
}
class FileListAdapter extends RecyclerView.Adapter<FileListAdapter.ViewHolder> {
private List<FileItem> items = new ArrayList<>();
void setItems(List<FileItem> items) {
this.items = items;
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_file, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(items.get(position));
}
@Override
public int getItemCount() {
return items.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
ImageView icon;
TextView name;
TextView info;
ViewHolder(@NonNull View itemView) {
super(itemView);
icon = itemView.findViewById(R.id.fileIcon);
name = itemView.findViewById(R.id.fileName);
info = itemView.findViewById(R.id.fileInfo);
}
void bind(FileItem item) {
name.setText(item.name);
if (item.isDirectory) {
icon.setImageResource(android.R.drawable.ic_menu_agenda);
info.setText("文件夹");
} else {
icon.setImageResource(android.R.drawable.ic_menu_save);
info.setText("SO文件");
}
if (!item.isReadable) {
itemView.setAlpha(0.5f);
} else {
itemView.setAlpha(1.0f);
}
itemView.setOnClickListener(v -> {
if ("..".equals(item.name)) {
// Go to parent directory
int lastSlash = currentPath.lastIndexOf('/');
if (lastSlash > 0) {
currentPath = currentPath.substring(0, lastSlash);
} else {
currentPath = "/";
}
loadFiles();
} else if (item.isDirectory) {
if (!item.isReadable) {
Toast.makeText(FileBrowserActivity.this,
"没有权限访问此目录", Toast.LENGTH_SHORT).show();
return;
}
if ("/".equals(currentPath)) {
currentPath = "/" + item.name;
} else {
currentPath = currentPath + "/" + item.name;
}
loadFiles();
} else {
// File selected
String selectedPath = currentPath + "/" + item.name;
Intent resultIntent = new Intent();
resultIntent.putExtra(EXTRA_SELECTED_PATH, selectedPath);
setResult(Activity.RESULT_OK, resultIntent);
finish();
}
});
}
}
}
}

View File

@@ -36,6 +36,7 @@ public class SoManagerFragment extends Fragment {
private List<ConfigManager.SoFile> globalSoFiles = new ArrayList<>();
private ActivityResultLauncher<Intent> filePickerLauncher;
private ActivityResultLauncher<Intent> fileBrowserLauncher;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -55,6 +56,19 @@ public class SoManagerFragment extends Fragment {
}
}
);
// Initialize file browser
fileBrowserLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
String path = result.getData().getStringExtra(FileBrowserActivity.EXTRA_SELECTED_PATH);
if (path != null) {
showDeleteOriginalDialog(path);
}
}
}
);
}
@Nullable
@@ -87,32 +101,16 @@ public class SoManagerFragment extends Fragment {
Toast.makeText(getContext(), "需要Root权限", Toast.LENGTH_LONG).show();
} else {
configManager.ensureModuleDirectories();
// Also ensure common directories exist
Shell.cmd("mkdir -p /data/local/tmp").exec();
Shell.cmd("chmod 777 /data/local/tmp").exec();
loadSoFiles();
}
}
private void loadSoFiles() {
// Load global SO files from the storage directory
Shell.Result result = Shell.cmd("ls -la " + ConfigManager.SO_STORAGE_DIR).exec();
globalSoFiles.clear();
if (result.isSuccess()) {
for (String line : result.getOut()) {
if (line.contains(".so")) {
// Parse file info
String[] parts = line.split("\\s+");
if (parts.length >= 9) {
String fileName = parts[parts.length - 1];
ConfigManager.SoFile soFile = new ConfigManager.SoFile();
soFile.name = fileName;
soFile.storedPath = ConfigManager.SO_STORAGE_DIR + "/" + fileName;
soFile.originalPath = soFile.storedPath; // For display
globalSoFiles.add(soFile);
}
}
}
}
// Load global SO files from config
globalSoFiles = configManager.getAllSoFiles();
updateUI();
}
@@ -128,12 +126,14 @@ public class SoManagerFragment extends Fragment {
}
private void showAddSoDialog() {
String[] options = {"文件管理器选择", "输入路径"};
String[] options = {"浏览文件系统", "从外部文件管理器选择", "手动输入路径"};
new MaterialAlertDialogBuilder(requireContext())
.setTitle("添加SO文件")
.setItems(options, (dialog, which) -> {
if (which == 0) {
openFileBrowser();
} else if (which == 1) {
openFilePicker();
} else {
showPathInputDialog();
@@ -142,6 +142,54 @@ public class SoManagerFragment extends Fragment {
.show();
}
private void openFileBrowser() {
// Show path selection dialog first
String[] paths = {
"/data/local/tmp",
"/sdcard",
"/sdcard/Download",
"/storage/emulated/0",
"自定义路径..."
};
new MaterialAlertDialogBuilder(requireContext())
.setTitle("选择起始目录")
.setItems(paths, (dialog, which) -> {
if (which == paths.length - 1) {
// Custom path
showCustomPathDialog();
} else {
Intent intent = new Intent(getContext(), FileBrowserActivity.class);
intent.putExtra(FileBrowserActivity.EXTRA_START_PATH, paths[which]);
intent.putExtra(FileBrowserActivity.EXTRA_FILE_FILTER, ".so");
fileBrowserLauncher.launch(intent);
}
})
.show();
}
private void showCustomPathDialog() {
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
android.widget.EditText editText = view.findViewById(android.R.id.edit);
editText.setText("/");
editText.setHint("输入起始路径");
new MaterialAlertDialogBuilder(requireContext())
.setTitle("自定义起始路径")
.setView(view)
.setPositiveButton("确定", (dialog, which) -> {
String path = editText.getText().toString().trim();
if (!path.isEmpty()) {
Intent intent = new Intent(getContext(), FileBrowserActivity.class);
intent.putExtra(FileBrowserActivity.EXTRA_START_PATH, path);
intent.putExtra(FileBrowserActivity.EXTRA_FILE_FILTER, ".so");
fileBrowserLauncher.launch(intent);
}
})
.setNegativeButton("取消", null)
.show();
}
private void openFilePicker() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
@@ -152,6 +200,7 @@ public class SoManagerFragment extends Fragment {
private void showPathInputDialog() {
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
android.widget.EditText editText = view.findViewById(android.R.id.edit);
editText.setText("/data/local/tmp/");
editText.setHint("/data/local/tmp/example.so");
new MaterialAlertDialogBuilder(requireContext())
@@ -160,7 +209,7 @@ public class SoManagerFragment extends Fragment {
.setPositiveButton("添加", (dialog, which) -> {
String path = editText.getText().toString().trim();
if (!path.isEmpty()) {
addSoFile(path, false);
showDeleteOriginalDialog(path);
}
})
.setNegativeButton("取消", null)
@@ -175,40 +224,38 @@ public class SoManagerFragment extends Fragment {
if (path.startsWith("file://")) {
path = path.substring(7);
}
addSoFile(path, false);
showDeleteOriginalDialog(path);
}
}
private void showDeleteOriginalDialog(String path) {
new MaterialAlertDialogBuilder(requireContext())
.setTitle("删除原文件")
.setMessage("是否删除原始SO文件\n\n文件路径" + path)
.setPositiveButton("删除原文件", (dialog, which) -> {
addSoFile(path, true);
})
.setNegativeButton("保留原文件", (dialog, which) -> {
addSoFile(path, false);
})
.setNeutralButton("取消", null)
.show();
}
private void addSoFile(String path, boolean deleteOriginal) {
// Verify file exists
Shell.Result result = Shell.cmd("test -f " + path + " && echo 'exists'").exec();
Shell.Result result = Shell.cmd("test -f \"" + path + "\" && echo 'exists'").exec();
if (!result.isSuccess() || result.getOut().isEmpty()) {
Toast.makeText(getContext(), "文件不存在: " + path, Toast.LENGTH_SHORT).show();
return;
}
// Generate unique filename
String fileName = new File(path).getName();
String storedPath = ConfigManager.SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
// Add to global SO files
configManager.addGlobalSoFile(path, deleteOriginal);
// Copy file
result = Shell.cmd("cp " + path + " " + storedPath).exec();
if (result.isSuccess()) {
ConfigManager.SoFile soFile = new ConfigManager.SoFile();
soFile.name = fileName;
soFile.storedPath = storedPath;
soFile.originalPath = path;
globalSoFiles.add(soFile);
if (deleteOriginal) {
Shell.cmd("rm " + path).exec();
}
updateUI();
Toast.makeText(getContext(), "SO文件已添加", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(getContext(), "复制文件失败", Toast.LENGTH_SHORT).show();
}
// Reload the list
loadSoFiles();
Toast.makeText(getContext(), "SO文件已添加", Toast.LENGTH_SHORT).show();
}
private void showDeleteConfirmation(ConfigManager.SoFile soFile) {
@@ -223,9 +270,8 @@ public class SoManagerFragment extends Fragment {
}
private void deleteSoFile(ConfigManager.SoFile soFile) {
Shell.cmd("rm " + soFile.storedPath).exec();
globalSoFiles.remove(soFile);
updateUI();
configManager.removeGlobalSoFile(soFile);
loadSoFiles();
Toast.makeText(getContext(), "SO文件已删除", Toast.LENGTH_SHORT).show();
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp" />
<TextView
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="暂无可用的SO文件\n请先在SO库管理中添加"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:id="@+id/currentPath"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurfaceVariant"
android:padding="12dp"
android:text="/data/local/tmp"
android:textSize="14sp"
android:fontFamily="monospace"
android:textColor="?attr/colorOnSurfaceVariant" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="4dp" />
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:alpha="0.3"
android:src="@android:drawable/ic_menu_search" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="此目录为空"
android:textSize="18sp"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -1,62 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="SO文件管理" />
</com.google.android.material.appbar.AppBarLayout>
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp"
android:visibility="gone">
android:orientation="vertical">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:alpha="0.3"
android:src="@drawable/ic_launcher_foreground" />
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无SO文件"
android:textSize="18sp"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="SO文件管理" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="点击右下角按钮添加SO文件"
android:textSize="14sp"
android:textColor="?android:attr/textColorTertiary" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp"
android:visibility="gone">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:alpha="0.3"
android:src="@drawable/ic_launcher_foreground" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="暂无SO文件"
android:textSize="18sp"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="点击右下角按钮添加SO文件"
android:textSize="14sp"
android:textColor="?android:attr/textColorTertiary" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="88dp"
android:padding="8dp" />
</FrameLayout>
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAdd"
android:layout_width="wrap_content"
@@ -66,4 +79,4 @@
android:src="@android:drawable/ic_input_add"
app:tint="@android:color/white" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/fileIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:padding="8dp"
android:src="@android:drawable/ic_menu_save"
android:tint="?attr/colorPrimary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/fileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:text="example.so"
android:singleLine="true"
android:ellipsize="middle" />
<TextView
android:id="@+id/fileInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:text="SO文件" />
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground">
<CheckBox
android:id="@+id/checkBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/textName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:text="example.so" />
<TextView
android:id="@+id/textPath"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:text="/data/local/tmp/example.so"
android:singleLine="true"
android:ellipsize="middle" />
</LinearLayout>
</LinearLayout>