add app manager logic
This commit is contained in:
@@ -24,10 +24,6 @@
|
||||
<activity
|
||||
android:name=".FileBrowserActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".AppSoConfigActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步加载应用列表
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
96
configapp/src/main/res/layout/dialog_app_config.xml
Normal file
96
configapp/src/main/res/layout/dialog_app_config.xml
Normal 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>
|
||||
Reference in New Issue
Block a user