add app manager logic

This commit is contained in:
jiqiu2021
2025-06-25 19:19:57 +08:00
parent 5632194bda
commit f557d71874
6 changed files with 273 additions and 190 deletions

View File

@@ -24,10 +24,6 @@
<activity
android:name=".FileBrowserActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".AppSoConfigActivity"
android:parentActivityName=".MainActivity" />
</application>
</manifest>

View File

@@ -22,11 +22,16 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.AppViewH
private List<AppInfo> appList;
private List<AppInfo> filteredAppList;
private OnAppToggleListener onAppToggleListener;
private OnAppClickListener onAppClickListener;
public interface OnAppToggleListener {
void onAppToggle(AppInfo appInfo, boolean isEnabled);
}
public interface OnAppClickListener {
void onAppClick(AppInfo appInfo);
}
public AppListAdapter() {
this.appList = new ArrayList<>();
this.filteredAppList = new ArrayList<>();
@@ -42,6 +47,10 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.AppViewH
this.onAppToggleListener = listener;
}
public void setOnAppClickListener(OnAppClickListener listener) {
this.onAppClickListener = listener;
}
public void filterApps(String query, boolean hideSystemApps) {
filteredAppList.clear();
@@ -120,6 +129,13 @@ public class AppListAdapter extends RecyclerView.Adapter<AppListAdapter.AppViewH
onAppToggleListener.onAppToggle(appInfo, isChecked);
}
});
// 设置整个item的点击监听器
itemView.setOnClickListener(v -> {
if (onAppClickListener != null) {
onAppClickListener.onAppClick(appInfo);
}
});
}
}
}

View File

@@ -10,6 +10,10 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.app.Dialog;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -18,6 +22,8 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.switchmaterial.SwitchMaterial;
import java.util.ArrayList;
import java.util.Collections;
@@ -27,7 +33,7 @@ import java.util.List;
/**
* 应用列表Fragment
*/
public class AppListFragment extends Fragment implements AppListAdapter.OnAppToggleListener {
public class AppListFragment extends Fragment implements AppListAdapter.OnAppToggleListener, AppListAdapter.OnAppClickListener {
private RecyclerView recyclerView;
private AppListAdapter adapter;
@@ -66,6 +72,7 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
private void setupRecyclerView() {
adapter = new AppListAdapter();
adapter.setOnAppToggleListener(this);
adapter.setOnAppClickListener(this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
}
@@ -111,6 +118,149 @@ public class AppListFragment extends Fragment implements AppListAdapter.OnAppTog
"App " + appInfo.getAppName() + " toggle: " + isEnabled);
}
@Override
public void onAppClick(AppInfo appInfo) {
showAppConfigDialog(appInfo);
}
private void showAppConfigDialog(AppInfo appInfo) {
View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.dialog_app_config, null);
// Set app info
ImageView appIcon = dialogView.findViewById(R.id.appIcon);
TextView appName = dialogView.findViewById(R.id.appName);
TextView packageName = dialogView.findViewById(R.id.packageName);
RecyclerView soListRecyclerView = dialogView.findViewById(R.id.soListRecyclerView);
TextView emptyText = dialogView.findViewById(R.id.emptyText);
SwitchMaterial switchHideInjection = dialogView.findViewById(R.id.switchHideInjection);
appIcon.setImageDrawable(appInfo.getAppIcon());
appName.setText(appInfo.getAppName());
packageName.setText(appInfo.getPackageName());
// Load current config
boolean hideInjection = configManager.getHideInjection();
switchHideInjection.setChecked(hideInjection);
// Setup SO list
List<ConfigManager.SoFile> globalSoFiles = configManager.getAllSoFiles();
List<ConfigManager.SoFile> appSoFiles = configManager.getAppSoFiles(appInfo.getPackageName());
if (globalSoFiles.isEmpty()) {
emptyText.setVisibility(View.VISIBLE);
soListRecyclerView.setVisibility(View.GONE);
} else {
emptyText.setVisibility(View.GONE);
soListRecyclerView.setVisibility(View.VISIBLE);
SoSelectionAdapter soAdapter = new SoSelectionAdapter(globalSoFiles, appSoFiles);
soListRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
soListRecyclerView.setAdapter(soAdapter);
}
// Create dialog
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext())
.setTitle("配置注入")
.setView(dialogView)
.setPositiveButton("保存", (dialog, which) -> {
// Save hide injection setting
configManager.setHideInjection(switchHideInjection.isChecked());
// Save SO selection
if (soListRecyclerView.getAdapter() != null) {
SoSelectionAdapter adapter = (SoSelectionAdapter) soListRecyclerView.getAdapter();
List<ConfigManager.SoFile> selectedSoFiles = adapter.getSelectedSoFiles();
// Clear existing SO files for this app
for (ConfigManager.SoFile existingSo : appSoFiles) {
configManager.removeSoFileFromApp(appInfo.getPackageName(), existingSo);
}
// Add selected SO files
for (ConfigManager.SoFile soFile : selectedSoFiles) {
configManager.addSoFileToApp(appInfo.getPackageName(), soFile);
}
}
})
.setNegativeButton("取消", null);
builder.show();
}
// Inner class for SO selection adapter
private static class SoSelectionAdapter extends RecyclerView.Adapter<SoSelectionAdapter.ViewHolder> {
private List<ConfigManager.SoFile> globalSoFiles;
private List<ConfigManager.SoFile> selectedSoFiles;
public SoSelectionAdapter(List<ConfigManager.SoFile> globalSoFiles, List<ConfigManager.SoFile> appSoFiles) {
this.globalSoFiles = globalSoFiles;
this.selectedSoFiles = new ArrayList<>(appSoFiles);
}
public List<ConfigManager.SoFile> getSelectedSoFiles() {
return new ArrayList<>(selectedSoFiles);
}
@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) {
ConfigManager.SoFile soFile = globalSoFiles.get(position);
holder.bind(soFile, selectedSoFiles);
}
@Override
public int getItemCount() {
return globalSoFiles.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, List<ConfigManager.SoFile> selectedList) {
nameText.setText(soFile.name);
pathText.setText(soFile.originalPath);
// Check if this SO is selected
boolean isSelected = false;
for (ConfigManager.SoFile selected : selectedList) {
if (selected.storedPath.equals(soFile.storedPath)) {
isSelected = true;
break;
}
}
checkBox.setOnCheckedChangeListener(null);
checkBox.setChecked(isSelected);
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
selectedList.add(soFile);
} else {
selectedList.removeIf(s -> s.storedPath.equals(soFile.storedPath));
}
});
itemView.setOnClickListener(v -> checkBox.toggle());
}
}
}
/**
* 异步加载应用列表
*/

View File

@@ -1,164 +0,0 @@
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

@@ -146,41 +146,30 @@ public class ConfigManager {
saveConfig();
}
public void addSoFileToApp(String packageName, String originalPath, boolean deleteOriginal) {
public void addSoFileToApp(String packageName, SoFile globalSoFile) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) {
appConfig = new AppConfig();
config.perAppConfig.put(packageName, appConfig);
}
// 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;
appConfig.soFiles.add(soFile);
if (deleteOriginal) {
Shell.cmd("rm \"" + originalPath + "\"").exec();
// Check if already added
for (SoFile existing : appConfig.soFiles) {
if (existing.storedPath.equals(globalSoFile.storedPath)) {
return; // Already added
}
saveConfig();
}
// Add reference to the global SO file
appConfig.soFiles.add(globalSoFile);
saveConfig();
}
public void removeSoFileFromApp(String packageName, SoFile soFile) {
AppConfig appConfig = config.perAppConfig.get(packageName);
if (appConfig == null) return;
appConfig.soFiles.remove(soFile);
// Delete the stored file
Shell.cmd("rm " + soFile.storedPath).exec();
appConfig.soFiles.removeIf(s -> s.storedPath.equals(soFile.storedPath));
saveConfig();
}

View File

@@ -0,0 +1,96 @@
<?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="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<ImageView
android:id="@+id/appIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_launcher_foreground" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/appName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold"
android:text="应用名称" />
<TextView
android:id="@+id/packageName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:text="com.example.app" />
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="选择要注入的SO文件"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginBottom="8dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/soListRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="200dp"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/emptyText"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:text="暂无可用的SO文件\n请先在SO库管理中添加"
android:textSize="14sp"
android:textColor="?android:attr/textColorTertiary"
android:visibility="gone" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchHideInjection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="隐藏注入"
android:layout_marginTop="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="使用Riru Hide隐藏注入的SO文件"
android:textSize="12sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginStart="56dp"
android:layout_marginTop="4dp" />
</LinearLayout>