feat:add so manager
This commit is contained in:
@@ -20,6 +20,14 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".FileBrowserActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AppSoConfigActivity"
|
||||||
|
android:parentActivityName=".MainActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</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);
|
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) {
|
public void addSoFileToApp(String packageName, String originalPath, boolean deleteOriginal) {
|
||||||
AppConfig appConfig = config.perAppConfig.get(packageName);
|
AppConfig appConfig = config.perAppConfig.get(packageName);
|
||||||
if (appConfig == null) {
|
if (appConfig == null) {
|
||||||
@@ -116,7 +158,7 @@ public class ConfigManager {
|
|||||||
String storedPath = SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
|
String storedPath = SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
|
||||||
|
|
||||||
// Copy SO file to our storage
|
// 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()) {
|
if (result.isSuccess()) {
|
||||||
SoFile soFile = new SoFile();
|
SoFile soFile = new SoFile();
|
||||||
soFile.name = fileName;
|
soFile.name = fileName;
|
||||||
@@ -125,7 +167,7 @@ public class ConfigManager {
|
|||||||
appConfig.soFiles.add(soFile);
|
appConfig.soFiles.add(soFile);
|
||||||
|
|
||||||
if (deleteOriginal) {
|
if (deleteOriginal) {
|
||||||
Shell.cmd("rm " + originalPath).exec();
|
Shell.cmd("rm \"" + originalPath + "\"").exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
saveConfig();
|
saveConfig();
|
||||||
@@ -155,6 +197,7 @@ public class ConfigManager {
|
|||||||
public static class ModuleConfig {
|
public static class ModuleConfig {
|
||||||
public boolean enabled = true;
|
public boolean enabled = true;
|
||||||
public boolean hideInjection = false;
|
public boolean hideInjection = false;
|
||||||
|
public List<SoFile> globalSoFiles = new ArrayList<>();
|
||||||
public Map<String, AppConfig> perAppConfig = new HashMap<>();
|
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 List<ConfigManager.SoFile> globalSoFiles = new ArrayList<>();
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> filePickerLauncher;
|
private ActivityResultLauncher<Intent> filePickerLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> fileBrowserLauncher;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
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
|
@Nullable
|
||||||
@@ -87,32 +101,16 @@ public class SoManagerFragment extends Fragment {
|
|||||||
Toast.makeText(getContext(), "需要Root权限", Toast.LENGTH_LONG).show();
|
Toast.makeText(getContext(), "需要Root权限", Toast.LENGTH_LONG).show();
|
||||||
} else {
|
} else {
|
||||||
configManager.ensureModuleDirectories();
|
configManager.ensureModuleDirectories();
|
||||||
|
// Also ensure common directories exist
|
||||||
|
Shell.cmd("mkdir -p /data/local/tmp").exec();
|
||||||
|
Shell.cmd("chmod 777 /data/local/tmp").exec();
|
||||||
loadSoFiles();
|
loadSoFiles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadSoFiles() {
|
private void loadSoFiles() {
|
||||||
// Load global SO files from the storage directory
|
// Load global SO files from config
|
||||||
Shell.Result result = Shell.cmd("ls -la " + ConfigManager.SO_STORAGE_DIR).exec();
|
globalSoFiles = configManager.getAllSoFiles();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,12 +126,14 @@ public class SoManagerFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showAddSoDialog() {
|
private void showAddSoDialog() {
|
||||||
String[] options = {"从文件管理器选择", "输入路径"};
|
String[] options = {"浏览文件系统", "从外部文件管理器选择", "手动输入路径"};
|
||||||
|
|
||||||
new MaterialAlertDialogBuilder(requireContext())
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle("添加SO文件")
|
.setTitle("添加SO文件")
|
||||||
.setItems(options, (dialog, which) -> {
|
.setItems(options, (dialog, which) -> {
|
||||||
if (which == 0) {
|
if (which == 0) {
|
||||||
|
openFileBrowser();
|
||||||
|
} else if (which == 1) {
|
||||||
openFilePicker();
|
openFilePicker();
|
||||||
} else {
|
} else {
|
||||||
showPathInputDialog();
|
showPathInputDialog();
|
||||||
@@ -142,6 +142,54 @@ public class SoManagerFragment extends Fragment {
|
|||||||
.show();
|
.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() {
|
private void openFilePicker() {
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
intent.setType("*/*");
|
intent.setType("*/*");
|
||||||
@@ -152,6 +200,7 @@ public class SoManagerFragment extends Fragment {
|
|||||||
private void showPathInputDialog() {
|
private void showPathInputDialog() {
|
||||||
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
|
View view = getLayoutInflater().inflate(R.layout.dialog_input, null);
|
||||||
android.widget.EditText editText = view.findViewById(android.R.id.edit);
|
android.widget.EditText editText = view.findViewById(android.R.id.edit);
|
||||||
|
editText.setText("/data/local/tmp/");
|
||||||
editText.setHint("/data/local/tmp/example.so");
|
editText.setHint("/data/local/tmp/example.so");
|
||||||
|
|
||||||
new MaterialAlertDialogBuilder(requireContext())
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
@@ -160,7 +209,7 @@ public class SoManagerFragment extends Fragment {
|
|||||||
.setPositiveButton("添加", (dialog, which) -> {
|
.setPositiveButton("添加", (dialog, which) -> {
|
||||||
String path = editText.getText().toString().trim();
|
String path = editText.getText().toString().trim();
|
||||||
if (!path.isEmpty()) {
|
if (!path.isEmpty()) {
|
||||||
addSoFile(path, false);
|
showDeleteOriginalDialog(path);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton("取消", null)
|
.setNegativeButton("取消", null)
|
||||||
@@ -175,40 +224,38 @@ public class SoManagerFragment extends Fragment {
|
|||||||
if (path.startsWith("file://")) {
|
if (path.startsWith("file://")) {
|
||||||
path = path.substring(7);
|
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) {
|
private void addSoFile(String path, boolean deleteOriginal) {
|
||||||
// Verify file exists
|
// 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()) {
|
if (!result.isSuccess() || result.getOut().isEmpty()) {
|
||||||
Toast.makeText(getContext(), "文件不存在: " + path, Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "文件不存在: " + path, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate unique filename
|
// Add to global SO files
|
||||||
String fileName = new File(path).getName();
|
configManager.addGlobalSoFile(path, deleteOriginal);
|
||||||
String storedPath = ConfigManager.SO_STORAGE_DIR + "/" + System.currentTimeMillis() + "_" + fileName;
|
|
||||||
|
|
||||||
// Copy file
|
// Reload the list
|
||||||
result = Shell.cmd("cp " + path + " " + storedPath).exec();
|
loadSoFiles();
|
||||||
if (result.isSuccess()) {
|
Toast.makeText(getContext(), "SO文件已添加", Toast.LENGTH_SHORT).show();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteConfirmation(ConfigManager.SoFile soFile) {
|
private void showDeleteConfirmation(ConfigManager.SoFile soFile) {
|
||||||
@@ -223,9 +270,8 @@ public class SoManagerFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void deleteSoFile(ConfigManager.SoFile soFile) {
|
private void deleteSoFile(ConfigManager.SoFile soFile) {
|
||||||
Shell.cmd("rm " + soFile.storedPath).exec();
|
configManager.removeGlobalSoFile(soFile);
|
||||||
globalSoFiles.remove(soFile);
|
loadSoFiles();
|
||||||
updateUI();
|
|
||||||
Toast.makeText(getContext(), "SO文件已删除", Toast.LENGTH_SHORT).show();
|
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,62 +1,75 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/emptyView"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:orientation="vertical">
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="32dp"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<ImageView
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:layout_width="96dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="96dp"
|
android:layout_height="wrap_content">
|
||||||
android:alpha="0.3"
|
|
||||||
android:src="@drawable/ic_launcher_foreground" />
|
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/toolbar"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_height="?attr/actionBarSize"
|
||||||
android:text="暂无SO文件"
|
app:title="SO文件管理" />
|
||||||
android:textSize="18sp"
|
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
|
||||||
|
|
||||||
<TextView
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
<FrameLayout
|
||||||
android:layout_marginTop="8dp"
|
android:layout_width="match_parent"
|
||||||
android:text="点击右下角按钮添加SO文件"
|
android:layout_height="match_parent">
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="?android:attr/textColorTertiary" />
|
<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>
|
</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
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
android:id="@+id/fabAdd"
|
android:id="@+id/fabAdd"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -66,4 +79,4 @@
|
|||||||
android:src="@android:drawable/ic_input_add"
|
android:src="@android:drawable/ic_input_add"
|
||||||
app:tint="@android:color/white" />
|
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