feat:add so manager
This commit is contained in:
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<>();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
// Reload the list
|
||||
loadSoFiles();
|
||||
Toast.makeText(getContext(), "SO文件已添加", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(getContext(), "复制文件失败", 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();
|
||||
}
|
||||
}
|
||||
42
configapp/src/main/res/layout/activity_app_so_config.xml
Normal file
42
configapp/src/main/res/layout/activity_app_so_config.xml
Normal 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>
|
||||
67
configapp/src/main/res/layout/activity_file_browser.xml
Normal file
67
configapp/src/main/res/layout/activity_file_browser.xml
Normal 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>
|
||||
@@ -1,6 +1,10 @@
|
||||
<?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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
@@ -17,6 +21,10 @@
|
||||
|
||||
</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"
|
||||
@@ -55,8 +63,13 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="88dp"
|
||||
android:padding="8dp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<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>
|
||||
44
configapp/src/main/res/layout/item_file.xml
Normal file
44
configapp/src/main/res/layout/item_file.xml
Normal 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>
|
||||
42
configapp/src/main/res/layout/item_so_selection.xml
Normal file
42
configapp/src/main/res/layout/item_so_selection.xml
Normal 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>
|
||||
Reference in New Issue
Block a user