Updated locations of nearly all files

This has been done in preparations for rewrite to kotlin and upcoming design changes.
Nothing should be broken but use caution.
This commit is contained in:
Viktor De Pasquale
2019-04-11 18:03:22 +02:00
parent f006a85fec
commit bea5308ab7
69 changed files with 338 additions and 333 deletions
@@ -0,0 +1,426 @@
package com.topjohnwu.magisk.model.adapters;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import com.buildware.widget.indeterm.IndeterminateCheckBox;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.view.ArrowExpandable;
import com.topjohnwu.magisk.view.Expandable;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.collection.ArraySet;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import java9.util.Comparators;
import java9.util.Lists;
import java9.util.Objects;
import java9.util.Sets;
import java9.util.stream.Collectors;
import java9.util.stream.Stream;
import java9.util.stream.StreamSupport;
public class ApplicationAdapter extends SectionedAdapter
<ApplicationAdapter.AppViewHolder, ApplicationAdapter.ProcessViewHolder> {
private static final String SAFETYNET_PROCESS = "com.google.android.gms.unstable";
private static final String GMS_PACKAGE = "com.google.android.gms";
private static boolean old_hide = false;
/* A list of apps that should not be shown as hide-able */
private static final List<String> HIDE_BLACKLIST = Lists.of(
App.self.getPackageName(),
"android",
"com.android.chrome",
"com.chrome.beta",
"com.chrome.dev",
"com.chrome.canary",
"com.android.webview",
"com.google.android.webview"
);
private static final List<String> DEFAULT_HIDELIST = Lists.of(
SAFETYNET_PROCESS
);
private static int BOTTOM_MARGIN = -1;
private List<HideAppInfo> fullList, showList;
private List<HideTarget> hideList;
private PackageManager pm;
private boolean showSystem;
public ApplicationAdapter(Context context) {
fullList = showList = Collections.emptyList();
hideList = Collections.emptyList();
pm = context.getPackageManager();
showSystem = Config.get(Config.Key.SHOW_SYSTEM_APP);
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
}
private static ViewGroup.MarginLayoutParams getMargins(RecyclerView.ViewHolder vh) {
return (ViewGroup.MarginLayoutParams) vh.itemView.getLayoutParams();
}
@Override
public int getSectionCount() {
return showList.size();
}
@Override
public int getItemCount(int section) {
HideAppInfo app = showList.get(section);
return app.expanded ? app.processList.size() : 0;
}
@Override
public AppViewHolder onCreateSectionViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_app, parent, false);
AppViewHolder vh = new AppViewHolder(v);
if (BOTTOM_MARGIN < 0)
BOTTOM_MARGIN = getMargins(vh).bottomMargin;
return vh;
}
@Override
public ProcessViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_process, parent, false);
return new ProcessViewHolder(v);
}
@Override
public void onBindSectionViewHolder(AppViewHolder holder, int section) {
HideAppInfo app = showList.get(section);
holder.app_name.setText(app.name);
holder.app_icon.setImageDrawable(app.info.loadIcon(pm));
holder.package_name.setText(app.info.packageName);
holder.checkBox.setOnStateChangedListener(null);
holder.checkBox.setState(app.getState());
holder.ex.setExpanded(app.expanded);
int index = getItemPosition(section, 0);
holder.checkBox.setOnStateChangedListener((IndeterminateCheckBox box, @Nullable Boolean status) -> {
if (status != null) {
setHide(status, app);
if (app.expanded)
notifyItemRangeChanged(index, app.processList.size());
}
});
if (app.processList.size() > 1) {
holder.arrow.setVisibility(View.VISIBLE);
holder.trigger.setOnClickListener((v) -> {
if (app.expanded) {
app.expanded = false;
notifyItemRangeRemoved(index, app.processList.size());
holder.ex.collapse();
} else {
app.expanded = true;
notifyItemRangeInserted(index, app.processList.size());
holder.ex.expand();
}
});
} else {
holder.arrow.setVisibility(View.GONE);
holder.trigger.setOnClickListener(null);
}
}
@Override
public void onBindItemViewHolder(ProcessViewHolder holder, int section, int position) {
HideAppInfo app = showList.get(section);
HideProcessInfo target = app.processList.get(position);
holder.process.setText(target.name);
holder.checkbox.setOnCheckedChangeListener(null);
holder.checkbox.setChecked(target.hidden);
holder.checkbox.setOnCheckedChangeListener((v, checked) -> {
setHide(checked, app, target);
notifyItemChanged(getSectionPosition(section));
});
getMargins(holder).bottomMargin =
position == app.processList.size() - 1 ? BOTTOM_MARGIN : 0;
}
public void filter(String constraint) {
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
Stream<HideAppInfo> s = StreamSupport.stream(fullList)
.filter(this::systemFilter)
.filter(t -> nameFilter(t, constraint));
UiThreadHandler.run(() -> {
showList = s.collect(Collectors.toList());
notifyDataSetChanged();
});
});
}
public void setShowSystem(boolean b) {
showSystem = b;
}
public void refresh() {
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
}
private void setHide(boolean add, HideAppInfo app) {
if (add) {
StreamSupport.stream(app.processList).forEach(p -> setHide(true, app, p));
} else {
if (StreamSupport.stream(app.processList)
.anyMatch(p -> p.name.equals(SAFETYNET_PROCESS))) {
StreamSupport.stream(app.processList).forEach(p -> setHide(false, app, p));
} else {
// Quick removal
Shell.su("magiskhide --rm " + app.info.packageName).submit();
StreamSupport.stream(app.processList).forEach(p -> p.hidden = false);
}
}
}
private void setHide(boolean add, HideAppInfo app, HideProcessInfo process) {
// Don't remove SafetyNet
if (!add && DEFAULT_HIDELIST.contains(process.name))
return;
Shell.su(Utils.fmt("magiskhide --%s %s %s", add ? "add" : "rm",
app.info.packageName, process.name)).submit();
process.hidden = add;
}
private void addProcesses(Set<String> set, ComponentInfo[] infos) {
if (infos != null)
for (ComponentInfo info : infos)
set.add(info.processName);
}
private PackageInfo getPackageInfo(String pkg) {
// Try super hard to get as much info as possible
try {
return pm.getPackageInfo(pkg,
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES |
PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS);
} catch (Exception e1) {
try {
PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES);
info.services = pm.getPackageInfo(pkg, PackageManager.GET_SERVICES).services;
info.receivers = pm.getPackageInfo(pkg, PackageManager.GET_RECEIVERS).receivers;
info.providers = pm.getPackageInfo(pkg, PackageManager.GET_PROVIDERS).providers;
return info;
} catch (Exception e2) {
return null;
}
}
}
@WorkerThread
private void loadApps() {
hideList = StreamSupport.stream(Shell.su("magiskhide --ls").exec().getOut())
.map(HideTarget::new)
.collect(Collectors.toList());
fullList = StreamSupport.stream(pm.getInstalledApplications(0))
.filter(info -> !HIDE_BLACKLIST.contains(info.packageName) && info.enabled)
.map(info -> {
if (old_hide) {
return new HideAppInfo(info, Sets.of(info.packageName));
} else {
Set<String> set = new ArraySet<>();
PackageInfo pkg = getPackageInfo(info.packageName);
if (pkg != null) {
addProcesses(set, pkg.activities);
addProcesses(set, pkg.services);
addProcesses(set, pkg.receivers);
addProcesses(set, pkg.providers);
}
if (set.isEmpty())
return null;
return new HideAppInfo(info, set);
}
}).filter(Objects::nonNull).sorted()
.collect(Collectors.toList());
Event.trigger(false, Event.MAGISK_HIDE_DONE);
}
// True if not system app or user already hidden it
private boolean systemFilter(HideAppInfo target) {
return showSystem || target.haveHidden() ||
(target.info.flags & ApplicationInfo.FLAG_SYSTEM) == 0;
}
private boolean contains(String s, String filter) {
return s.toLowerCase().contains(filter);
}
private boolean nameFilter(HideAppInfo target, String filter) {
if (filter == null || filter.isEmpty())
return true;
filter = filter.toLowerCase();
if (contains(target.name, filter))
return true;
for (HideProcessInfo p : target.processList) {
if (contains(p.name, filter))
return true;
}
return contains(target.info.packageName, filter);
}
class HideAppInfo implements Comparable<HideAppInfo> {
String name;
ApplicationInfo info;
List<HideProcessInfo> processList;
boolean expanded;
IndeterminateCheckBox.OnStateChangedListener listener;
HideAppInfo(ApplicationInfo appInfo, Set<String> set) {
info = appInfo;
name = Utils.getAppLabel(info, pm);
expanded = false;
processList = StreamSupport.stream(set)
.map(process -> new HideProcessInfo(info.packageName, process))
.sorted().collect(Collectors.toList());
listener = (IndeterminateCheckBox box, @Nullable Boolean status) -> {
if (status != null) {
for (HideProcessInfo p : processList) {
String cmd = Utils.fmt("magiskhide --%s %s %s",
status ? "add" : "rm", info.packageName, p.name);
Shell.su(cmd).submit();
p.hidden = status;
}
}
};
}
@Override
public int compareTo(HideAppInfo o) {
Comparator<HideAppInfo> c;
c = Comparators.comparing(HideAppInfo::haveHidden);
c = Comparators.reversed(c);
c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase);
c = Comparators.thenComparing(c, t -> t.info.packageName);
return c.compare(this, o);
}
Boolean getState() {
boolean all = true;
boolean hidden = false;
for (HideProcessInfo p : processList) {
if (!p.hidden)
all = false;
else
hidden = true;
}
if (all)
return true;
return hidden ? null : false;
}
boolean haveHidden() {
Boolean c = getState();
return c == null ? true : c;
}
}
class HideProcessInfo implements Comparable<HideProcessInfo> {
String name;
boolean hidden;
HideProcessInfo(String pkg, String process) {
this.name = process;
for (HideTarget t : hideList) {
if (t.pkg.equals(pkg) && t.process.equals(process)) {
hidden = true;
break;
}
}
}
@Override
public int compareTo(HideProcessInfo o) {
Comparator<HideProcessInfo> c;
c = Comparators.comparing((HideProcessInfo t) -> t.hidden);
c = Comparators.reversed(c);
c = Comparators.thenComparing(c, t -> t.name);
return c.compare(this, o);
}
}
class HideTarget {
String pkg;
String process;
HideTarget(String line) {
String[] split = line.split("\\|", 2);
pkg = split[0];
if (split.length == 2) {
process = split[1];
} else {
// Backwards compatibility
old_hide = true;
process = pkg;
}
}
}
class AppViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.app_icon) ImageView app_icon;
@BindView(R.id.app_name) TextView app_name;
@BindView(R.id.package_name) TextView package_name;
@BindView(R.id.checkbox) IndeterminateCheckBox checkBox;
@BindView(R.id.trigger) View trigger;
@BindView(R.id.arrow) ImageView arrow;
Expandable ex;
AppViewHolder(@NonNull View itemView) {
super(itemView);
new ApplicationAdapter$AppViewHolder_ViewBinding(this, itemView);
ex = new ArrowExpandable(new Expandable() {
@Override
protected void onExpand() {
getMargins(AppViewHolder.this).bottomMargin = 0;
}
@Override
protected void onCollapse() {
getMargins(AppViewHolder.this).bottomMargin = BOTTOM_MARGIN;
}
}, arrow);
}
}
class ProcessViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.process) TextView process;
@BindView(R.id.checkbox) CheckBox checkbox;
ProcessViewHolder(@NonNull View itemView) {
super(itemView);
new ApplicationAdapter$ProcessViewHolder_ViewBinding(this, itemView);
}
}
}
@@ -0,0 +1,126 @@
package com.topjohnwu.magisk.model.adapters;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.android.material.snackbar.Snackbar;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.entity.Module;
import com.topjohnwu.magisk.view.SnackbarMaker;
import com.topjohnwu.superuser.Shell;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
private final List<Module> mList;
public ModulesAdapter(List<Module> list) {
mList = list;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
Context context = holder.itemView.getContext();
final Module module = mList.get(position);
String version = module.getVersion();
String author = module.getAuthor();
String description = module.getDescription();
String noInfo = context.getString(R.string.no_info_provided);
holder.title.setText(module.getName());
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
holder.checkBox.setOnCheckedChangeListener(null);
holder.checkBox.setChecked(module.isEnabled());
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
int snack;
if (isChecked) {
module.removeDisableFile();
snack = R.string.disable_file_removed;
} else {
module.createDisableFile();
snack = R.string.disable_file_created;
}
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
});
holder.delete.setOnClickListener(v -> {
boolean removed = module.willBeRemoved();
int snack;
if (removed) {
module.deleteRemoveFile();
snack = R.string.remove_file_deleted;
} else {
module.createRemoveFile();
snack = R.string.remove_file_created;
}
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
updateDeleteButton(holder, module);
});
if (module.isUpdated()) {
holder.notice.setVisibility(View.VISIBLE);
holder.notice.setText(R.string.update_file_created);
holder.delete.setEnabled(false);
} else {
updateDeleteButton(holder, module);
}
}
private void updateDeleteButton(ViewHolder holder, Module module) {
holder.notice.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE);
if (module.willBeRemoved()) {
holder.delete.setImageResource(R.drawable.ic_undelete);
} else {
holder.delete.setImageResource(R.drawable.ic_delete);
}
}
@Override
public int getItemCount() {
return mList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.title) TextView title;
@BindView(R.id.version_name) TextView versionName;
@BindView(R.id.description) TextView description;
@BindView(R.id.notice) TextView notice;
@BindView(R.id.checkbox) CheckBox checkBox;
@BindView(R.id.author) TextView author;
@BindView(R.id.delete) ImageView delete;
ViewHolder(View itemView) {
super(itemView);
new ModulesAdapter$ViewHolder_ViewBinding(this, itemView);
if (!Shell.rootAccess()) {
checkBox.setEnabled(false);
delete.setEnabled(false);
}
}
}
}
@@ -0,0 +1,171 @@
package com.topjohnwu.magisk.model.adapters;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.android.material.snackbar.Snackbar;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.data.database.MagiskDB;
import com.topjohnwu.magisk.model.entity.Policy;
import com.topjohnwu.magisk.utils.FingerprintHelper;
import com.topjohnwu.magisk.view.ArrowExpandable;
import com.topjohnwu.magisk.view.Expandable;
import com.topjohnwu.magisk.view.ExpandableViewHolder;
import com.topjohnwu.magisk.view.SnackbarMaker;
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog;
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SwitchCompat;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
private List<Policy> policyList;
private MagiskDB dbHelper;
private PackageManager pm;
private boolean[] expandList;
public PolicyAdapter(List<Policy> list, MagiskDB db, PackageManager pm) {
policyList = list;
expandList = new boolean[policyList.size()];
dbHelper = db;
this.pm = pm;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_policy, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Policy policy = policyList.get(position);
holder.settings.setExpanded(expandList[position]);
holder.trigger.setOnClickListener(view -> {
if (holder.settings.isExpanded()) {
holder.settings.collapse();
expandList[position] = false;
} else {
holder.settings.expand();
expandList[position] = true;
}
});
holder.appName.setText(policy.appName);
holder.packageName.setText(policy.packageName);
holder.appIcon.setImageDrawable(policy.info.loadIcon(pm));
holder.notificationSwitch.setOnCheckedChangeListener(null);
holder.loggingSwitch.setOnCheckedChangeListener(null);
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
holder.notificationSwitch.setChecked(policy.notification);
holder.loggingSwitch.setChecked(policy.logging);
holder.masterSwitch.setOnClickListener(v -> {
boolean isChecked = holder.masterSwitch.isChecked();
Runnable r = () -> {
if ((isChecked && policy.policy == Policy.DENY) ||
(!isChecked && policy.policy == Policy.ALLOW)) {
policy.policy = isChecked ? Policy.ALLOW : Policy.DENY;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
};
if (FingerprintHelper.useFingerprint()) {
holder.masterSwitch.setChecked(!isChecked);
new FingerprintAuthDialog((Activity) v.getContext(), () -> {
holder.masterSwitch.setChecked(isChecked);
r.run();
}).show();
} else {
r.run();
}
});
holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> {
if ((isChecked && !policy.notification) ||
(!isChecked && policy.notification)) {
policy.notification = isChecked;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
});
holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> {
if ((isChecked && !policy.logging) ||
(!isChecked && policy.logging)) {
policy.logging = isChecked;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
});
holder.delete.setOnClickListener(v -> {
DialogInterface.OnClickListener l = (dialog, which) -> {
policyList.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, policyList.size());
SnackbarMaker.make(holder.itemView, v.getContext().getString(R.string.su_snack_revoke, policy.appName),
Snackbar.LENGTH_SHORT).show();
dbHelper.deletePolicy(policy);
};
if (FingerprintHelper.useFingerprint()) {
new FingerprintAuthDialog((Activity) v.getContext(),
() -> l.onClick(null, 0)).show();
} else {
new CustomAlertDialog((Activity) v.getContext())
.setTitle(R.string.su_revoke_title)
.setMessage(v.getContext().getString(R.string.su_revoke_msg, policy.appName))
.setPositiveButton(R.string.yes, l)
.setNegativeButton(R.string.no_thanks, null)
.setCancelable(true)
.show();
}
});
}
@Override
public int getItemCount() {
return policyList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.app_name) TextView appName;
@BindView(R.id.package_name) TextView packageName;
@BindView(R.id.app_icon) ImageView appIcon;
@BindView(R.id.master_switch) SwitchCompat masterSwitch;
@BindView(R.id.notification_switch) SwitchCompat notificationSwitch;
@BindView(R.id.logging_switch) SwitchCompat loggingSwitch;
@BindView(R.id.expand_layout) ViewGroup expandLayout;
@BindView(R.id.arrow) ImageView arrow;
@BindView(R.id.trigger) View trigger;
@BindView(R.id.delete) ImageView delete;
@BindView(R.id.more_info) ImageView moreInfo;
Expandable settings;
public ViewHolder(View itemView) {
super(itemView);
new PolicyAdapter$ViewHolder_ViewBinding(this, itemView);
settings = new ArrowExpandable(new ExpandableViewHolder(expandLayout), arrow);
}
}
}
@@ -0,0 +1,245 @@
package com.topjohnwu.magisk.model.adapters;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Build;
import android.text.TextUtils;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SearchView;
import android.widget.TextView;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.model.download.DownloadModuleService;
import com.topjohnwu.magisk.model.entity.Module;
import com.topjohnwu.magisk.model.entity.Repo;
import com.topjohnwu.magisk.ui.base.BaseActivity;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.view.MarkDownWindow;
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import java9.util.stream.StreamSupport;
public class ReposAdapter
extends SectionedAdapter<ReposAdapter.SectionHolder, ReposAdapter.RepoHolder>
implements Event.AutoListener, SearchView.OnQueryTextListener {
private static final int UPDATES = 0;
private static final int INSTALLED = 1;
private static final int OTHERS = 2;
private Map<String, Module> moduleMap;
private RepoDatabaseHelper repoDB;
private List<Pair<Integer, List<Repo>>> repoPairs;
private List<Repo> fullList;
private SearchView mSearch;
public ReposAdapter() {
repoDB = App.self.repoDB;
moduleMap = Collections.emptyMap();
fullList = Collections.emptyList();
repoPairs = new ArrayList<>();
}
@Override
public int getSectionCount() {
return repoPairs.size();
}
@Override
public int getItemCount(int section) {
return repoPairs.get(section).second.size();
}
@Override
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.section, parent, false);
return new SectionHolder(v);
}
@Override
public RepoHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false);
return new RepoHolder(v);
}
@Override
public void onBindSectionViewHolder(SectionHolder holder, int section) {
switch (repoPairs.get(section).first) {
case UPDATES:
holder.sectionText.setText(R.string.update_available);
break;
case INSTALLED:
holder.sectionText.setText(R.string.installed);
break;
case OTHERS:
holder.sectionText.setText(R.string.not_installed);
break;
}
}
@Override
public void onBindItemViewHolder(RepoHolder holder, int section, int position) {
Repo repo = repoPairs.get(section).second.get(position);
Context context = holder.itemView.getContext();
String name = repo.getName();
String version = repo.getVersion();
String author = repo.getAuthor();
String description = repo.getDescription();
String noInfo = context.getString(R.string.no_info_provided);
holder.title.setText(TextUtils.isEmpty(name) ? noInfo : name);
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString()));
holder.infoLayout.setOnClickListener(v ->
MarkDownWindow.show((BaseActivity) context, null, repo.getDetailUrl()));
holder.downloadImage.setOnClickListener(v -> {
new CustomAlertDialog((BaseActivity) context)
.setTitle(context.getString(R.string.repo_install_title, repo.getName()))
.setMessage(context.getString(R.string.repo_install_msg, repo.getDownloadFilename()))
.setCancelable(true)
.setPositiveButton(R.string.install, (d, i) ->
startDownload((BaseActivity) context, repo, true))
.setNeutralButton(R.string.download, (d, i) ->
startDownload((BaseActivity) context, repo, false))
.setNegativeButton(R.string.no_thanks, null)
.show();
});
}
private void startDownload(BaseActivity activity, Repo repo, Boolean install) {
activity.runWithExternalRW(() -> {
Intent intent = new Intent(activity, ClassMap.get(DownloadModuleService.class))
.putExtra("repo", repo).putExtra("install", install);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intent);
} else {
activity.startService(intent);
}
});
}
private void updateLists() {
if (mSearch != null)
onQueryTextChange(mSearch.getQuery().toString());
else
onQueryTextChange("");
}
private static boolean noCaseContain(String a, String b) {
return a.toLowerCase().contains(b.toLowerCase());
}
public void setSearchView(SearchView view) {
mSearch = view;
mSearch.setOnQueryTextListener(this);
}
public void notifyDBChanged(boolean refresh) {
try (Cursor c = repoDB.getRepoCursor()) {
fullList = new ArrayList<>(c.getCount());
while (c.moveToNext())
fullList.add(new Repo(c));
}
if (refresh)
updateLists();
}
@Override
public void onEvent(int event) {
moduleMap = Event.getResult(event);
updateLists();
}
@Override
public int[] getListeningEvents() {
return new int[] {Event.MODULE_LOAD_DONE};
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String s) {
List<Repo> updates = new ArrayList<>();
List<Repo> installed = new ArrayList<>();
List<Repo> others = new ArrayList<>();
StreamSupport.stream(fullList)
.filter(repo -> noCaseContain(repo.getName(), s)
|| noCaseContain(repo.getAuthor(), s)
|| noCaseContain(repo.getDescription(), s))
.forEach(repo -> {
Module module = moduleMap.get(repo.getId());
if (module != null) {
if (repo.getVersionCode() > module.getVersionCode()) {
// Updates
updates.add(repo);
} else {
installed.add(repo);
}
} else {
others.add(repo);
}
});
repoPairs.clear();
if (!updates.isEmpty())
repoPairs.add(new Pair<>(UPDATES, updates));
if (!installed.isEmpty())
repoPairs.add(new Pair<>(INSTALLED, installed));
if (!others.isEmpty())
repoPairs.add(new Pair<>(OTHERS, others));
notifyDataSetChanged();
return false;
}
static class SectionHolder extends RecyclerView.ViewHolder {
@BindView(R.id.section_text) TextView sectionText;
SectionHolder(View itemView) {
super(itemView);
new ReposAdapter$SectionHolder_ViewBinding(this, itemView);
}
}
static class RepoHolder extends RecyclerView.ViewHolder {
@BindView(R.id.title) TextView title;
@BindView(R.id.version_name) TextView versionName;
@BindView(R.id.description) TextView description;
@BindView(R.id.author) TextView author;
@BindView(R.id.info_layout) View infoLayout;
@BindView(R.id.download) ImageView downloadImage;
@BindView(R.id.update_time) TextView updateTime;
RepoHolder(View itemView) {
super(itemView);
new ReposAdapter$RepoHolder_ViewBinding(this, itemView);
}
}
}
@@ -0,0 +1,96 @@
package com.topjohnwu.magisk.model.adapters;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public abstract class SectionedAdapter<S extends RecyclerView.ViewHolder, C extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int SECTION_TYPE = Integer.MIN_VALUE;
@NonNull
@Override
final public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == SECTION_TYPE)
return onCreateSectionViewHolder(parent);
return onCreateItemViewHolder(parent, viewType);
}
@Override
@SuppressWarnings("unchecked")
final public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
PositionInfo info = getPositionInfo(position);
if (info.position == -1)
onBindSectionViewHolder((S) holder, info.section);
else
onBindItemViewHolder((C) holder, info.section, info.position);
}
@Override
final public int getItemCount() {
int size, sec;
size = sec = getSectionCount();
for (int i = 0; i < sec; ++i){
size += getItemCount(i);
}
return size;
}
@Override
final public int getItemViewType(int position) {
PositionInfo info = getPositionInfo(position);
if (info.position == -1)
return SECTION_TYPE;
else
return getItemViewType(info.section, info.position);
}
public int getItemViewType(int section, int position) {
return 0;
}
protected int getSectionPosition(int section) {
return getItemPosition(section, -1);
}
protected int getItemPosition(int section, int position) {
int realPosition = 0;
// Previous sections
for (int i = 0; i < section; ++i) {
realPosition += getItemCount(i) + 1;
}
// Current section
realPosition += position + 1;
return realPosition;
}
private PositionInfo getPositionInfo(int position) {
int section = 0;
while (true) {
if (position == 0)
return new PositionInfo(section, -1);
position -= 1;
if (position < getItemCount(section))
return new PositionInfo(section, position);
position -= getItemCount(section++);
}
}
private static class PositionInfo {
int section;
int position;
PositionInfo(int section, int position) {
this.section = section;
this.position = position;
}
}
public abstract int getSectionCount();
public abstract int getItemCount(int section);
public abstract S onCreateSectionViewHolder(ViewGroup parent);
public abstract C onCreateItemViewHolder(ViewGroup parent, int viewType);
public abstract void onBindSectionViewHolder(S holder, int section);
public abstract void onBindItemViewHolder(C holder, int section, int position);
}
@@ -0,0 +1,103 @@
package com.topjohnwu.magisk.model.adapters;
import android.app.Activity;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public abstract class StringListAdapter<VH extends StringListAdapter.ViewHolder>
extends RecyclerView.Adapter<VH> {
private RecyclerView rv;
private boolean dynamic;
private int screenWidth;
private int txtWidth = -1;
private int padding;
protected List<String> mList;
public StringListAdapter(List<String> list) {
this(list, false);
}
public StringListAdapter(List<String> list, boolean isDynamic) {
mList = list;
dynamic = isDynamic;
}
@NonNull
@Override
public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayoutRes(), parent, false);
VH vh = createViewHolder(v);
if (txtWidth < 0)
onUpdateTextWidth(vh);
return vh;
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
holder.txt.setText(mList.get(position));
holder.txt.getLayoutParams().width = txtWidth;
if (dynamic)
onUpdateTextWidth(holder);
}
protected void onUpdateTextWidth(VH vh) {
if (txtWidth < 0) {
txtWidth = screenWidth - padding;
} else {
vh.txt.measure(0, 0);
int width = vh.txt.getMeasuredWidth();
if (width > txtWidth) {
txtWidth = width;
vh.txt.getLayoutParams().width = txtWidth;
}
}
if (rv.getWidth() != txtWidth + padding)
rv.requestLayout();
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView rv) {
DisplayMetrics displayMetrics = new DisplayMetrics();
((Activity) rv.getContext()).getWindowManager()
.getDefaultDisplay().getMetrics(displayMetrics);
screenWidth = displayMetrics.widthPixels;
padding = rv.getPaddingStart() + rv.getPaddingEnd();
this.rv = rv;
}
@Override
public final int getItemCount() {
return mList.size();
}
@LayoutRes
protected abstract int itemLayoutRes();
@NonNull
public abstract VH createViewHolder(@NonNull View v);
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
public TextView txt;
public ViewHolder(@NonNull View itemView) {
super(itemView);
txt = itemView.findViewById(textViewResId());
}
@IdRes
protected abstract int textViewResId();
}
}
@@ -0,0 +1,143 @@
package com.topjohnwu.magisk.model.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.TextView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.data.database.MagiskDB;
import com.topjohnwu.magisk.model.entity.SuLogEntry;
import com.topjohnwu.magisk.view.ExpandableViewHolder;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
public class SuLogAdapter extends SectionedAdapter<SuLogAdapter.SectionHolder, SuLogAdapter.LogViewHolder> {
private List<List<SuLogEntry>> logEntries;
private Set<Integer> itemExpanded, sectionExpanded;
private MagiskDB suDB;
public SuLogAdapter(MagiskDB db) {
suDB = db;
logEntries = Collections.emptyList();
sectionExpanded = new HashSet<>();
itemExpanded = new HashSet<>();
}
@Override
public int getSectionCount() {
return logEntries.size();
}
@Override
public int getItemCount(int section) {
return sectionExpanded.contains(section) ? logEntries.get(section).size() : 0;
}
@Override
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false);
return new SectionHolder(v);
}
@Override
public LogViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false);
return new LogViewHolder(v);
}
@Override
public void onBindSectionViewHolder(SectionHolder holder, int section) {
SuLogEntry entry = logEntries.get(section).get(0);
holder.arrow.setRotation(sectionExpanded.contains(section) ? 180 : 0);
holder.itemView.setOnClickListener(v -> {
RotateAnimation rotate;
if (sectionExpanded.contains(section)) {
holder.arrow.setRotation(0);
rotate = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sectionExpanded.remove(section);
notifyItemRangeRemoved(getItemPosition(section, 0), logEntries.get(section).size());
} else {
rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sectionExpanded.add(section);
notifyItemRangeInserted(getItemPosition(section, 0), logEntries.get(section).size());
}
rotate.setDuration(300);
rotate.setFillAfter(true);
holder.arrow.setAnimation(rotate);
});
holder.date.setText(entry.getDateString());
}
@Override
public void onBindItemViewHolder(LogViewHolder holder, int section, int position) {
SuLogEntry entry = logEntries.get(section).get(position);
int realIdx = getItemPosition(section, position);
holder.expandable.setExpanded(itemExpanded.contains(realIdx));
holder.itemView.setOnClickListener(view -> {
if (holder.expandable.isExpanded()) {
holder.expandable.collapse();
itemExpanded.remove(realIdx);
} else {
holder.expandable.expand();
itemExpanded.add(realIdx);
}
});
Context context = holder.itemView.getContext();
holder.appName.setText(entry.appName);
holder.action.setText(entry.action ? R.string.grant : R.string.deny);
holder.pid.setText(context.getString(R.string.pid, entry.fromPid));
holder.uid.setText(context.getString(R.string.target_uid, entry.toUid));
holder.command.setText(context.getString(R.string.command, entry.command));
holder.time.setText(entry.getTimeString());
}
public void notifyDBChanged() {
logEntries = suDB.getLogs();
itemExpanded.clear();
sectionExpanded.clear();
sectionExpanded.add(0);
notifyDataSetChanged();
}
static class SectionHolder extends RecyclerView.ViewHolder {
@BindView(R.id.date) TextView date;
@BindView(R.id.arrow) ImageView arrow;
SectionHolder(View itemView) {
super(itemView);
new SuLogAdapter$SectionHolder_ViewBinding(this, itemView);
}
}
static class LogViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.app_name) TextView appName;
@BindView(R.id.action) TextView action;
@BindView(R.id.time) TextView time;
@BindView(R.id.pid) TextView pid;
@BindView(R.id.uid) TextView uid;
@BindView(R.id.cmd) TextView command;
@BindView(R.id.expand_layout) ViewGroup expandLayout;
ExpandableViewHolder expandable;
LogViewHolder(View itemView) {
super(itemView);
new SuLogAdapter$LogViewHolder_ViewBinding(this, itemView);
expandable = new ExpandableViewHolder(expandLayout);
}
}
}
@@ -0,0 +1,41 @@
package com.topjohnwu.magisk.model.adapters;
import java.util.ArrayList;
import java.util.List;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
public class TabFragmentAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList;
private List<String> titleList;
public TabFragmentAdapter(FragmentManager fm) {
super(fm);
fragmentList = new ArrayList<>();
titleList = new ArrayList<>();
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titleList.get(position);
}
public void addTab(Fragment fragment, String title) {
fragmentList.add(fragment);
titleList.add(title);
}
}
@@ -0,0 +1,159 @@
package com.topjohnwu.magisk.model.download;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.model.entity.Repo;
import com.topjohnwu.magisk.ui.flash.FlashActivity;
import com.topjohnwu.magisk.view.Notifications;
import com.topjohnwu.magisk.view.ProgressNotification;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import androidx.annotation.Nullable;
public class DownloadModuleService extends Service {
private List<ProgressNotification> notifications;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
notifications = new ArrayList<>();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Shell.EXECUTOR.execute(() -> {
Repo repo = intent.getParcelableExtra("repo");
boolean install = intent.getBooleanExtra("install", false);
dlProcessInstall(repo, install);
});
return START_REDELIVER_INTENT;
}
@Override
public synchronized void onTaskRemoved(Intent rootIntent) {
for (ProgressNotification n : notifications) {
Notifications.mgr.cancel(n.hashCode());
}
notifications.clear();
}
private synchronized void addNotification(ProgressNotification n) {
if (notifications.isEmpty()) {
// Start foreground
startForeground(n.hashCode(), n.getNotification());
}
notifications.add(n);
}
private synchronized void removeNotification(ProgressNotification n) {
notifications.remove(n);
if (notifications.isEmpty()) {
// No more tasks, stop service
stopForeground(true);
stopSelf();
} else {
// Pick another notification as our foreground notification
n = notifications.get(0);
startForeground(n.hashCode(), n.getNotification());
}
}
private void dlProcessInstall(Repo repo, boolean install) {
File output = new File(Const.EXTERNAL_PATH, repo.getDownloadFilename());
ProgressNotification progress = new ProgressNotification(output.getName());
addNotification(progress);
try {
InputStream in = Networking.get(repo.getZipUrl())
.setDownloadProgressListener(progress)
.execForInputStream().getResult();
OutputStream out = new BufferedOutputStream(new FileOutputStream(output));
processZip(in, out, repo.isNewInstaller());
Intent intent = new Intent(this, ClassMap.get(FlashActivity.class));
intent.setData(Uri.fromFile(output))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
synchronized (getApplication()) {
if (install && App.foreground() != null &&
!(App.foreground() instanceof FlashActivity)) {
/* Only start flashing if there is a foreground activity and the
* user is not also flashing another module at the same time */
App.foreground().startActivity(intent);
} else {
/* Or else we preset a notification notifying that we are done */
PendingIntent pi = PendingIntent.getActivity(this, progress.hashCode(), intent,
PendingIntent.FLAG_UPDATE_CURRENT);
progress.dlDone(pi);
}
}
} catch (Exception e) {
e.printStackTrace();
progress.dlFail();
}
removeNotification(progress);
}
private void processZip(InputStream in, OutputStream out, boolean inject)
throws IOException {
try (ZipInputStream zin = new ZipInputStream(in);
ZipOutputStream zout = new ZipOutputStream(out)) {
if (inject) {
// Inject latest module-installer.sh as update-binary
zout.putNextEntry(new ZipEntry("META-INF/"));
zout.putNextEntry(new ZipEntry("META-INF/com/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/update-binary"));
try (InputStream update_bin = Networking.get(Const.Url.MODULE_INSTALLER)
.execForInputStream().getResult()) {
ShellUtils.pump(update_bin, zout);
}
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/updater-script"));
zout.write("#MAGISK\n".getBytes(StandardCharsets.UTF_8));
}
int off = -1;
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
if (off < 0)
off = entry.getName().indexOf('/') + 1;
String path = entry.getName().substring(off);
if (path.isEmpty())
continue;
if (inject && path.startsWith("META-INF"))
continue;
zout.putNextEntry(new ZipEntry(path));
if (!entry.isDirectory())
ShellUtils.pump(zin, zout);
}
}
}
}
@@ -0,0 +1,144 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.List;
import androidx.annotation.NonNull;
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
private String mId, mName, mVersion, mAuthor, mDescription;
private int mVersionCode = -1;
protected BaseModule() {
mId = mName = mVersion = mAuthor = mDescription = "";
}
protected BaseModule(Cursor c) {
mId = nonNull(c.getString(c.getColumnIndex("id")));
mName = nonNull(c.getString(c.getColumnIndex("name")));
mVersion = nonNull(c.getString(c.getColumnIndex("version")));
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
mAuthor = nonNull(c.getString(c.getColumnIndex("author")));
mDescription = nonNull(c.getString(c.getColumnIndex("description")));
}
protected BaseModule(Parcel p) {
mId = p.readString();
mName = p.readString();
mVersion = p.readString();
mAuthor = p.readString();
mDescription = p.readString();
mVersionCode = p.readInt();
}
@Override
public int compareTo(@NonNull BaseModule module) {
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mId);
dest.writeString(mName);
dest.writeString(mVersion);
dest.writeString(mAuthor);
dest.writeString(mDescription);
dest.writeInt(mVersionCode);
}
private String nonNull(String s) {
return s == null ? "" : s;
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("id", mId);
values.put("name", mName);
values.put("version", mVersion);
values.put("versionCode", mVersionCode);
values.put("author", mAuthor);
values.put("description", mDescription);
return values;
}
protected void parseProps(List<String> props) { parseProps(props.toArray(new String[0])); }
protected void parseProps(String[] props) throws NumberFormatException {
for (String line : props) {
String[] prop = line.split("=", 2);
if (prop.length != 2)
continue;
String key = prop[0].trim();
String value = prop[1].trim();
if (key.isEmpty() || key.charAt(0) == '#')
continue;
switch (key) {
case "id":
mId = value;
break;
case "name":
mName = value;
break;
case "version":
mVersion = value;
break;
case "versionCode":
mVersionCode = Integer.parseInt(value);
break;
case "author":
mAuthor = value;
break;
case "description":
mDescription = value;
break;
default:
break;
}
}
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public String getVersion() {
return mVersion;
}
public String getAuthor() {
return mAuthor;
}
public String getId() {
return mId;
}
public void setId(String id) {
mId = id;
}
public String getDescription() {
return mDescription;
}
public int getVersionCode() {
return mVersionCode;
}
}
@@ -0,0 +1,79 @@
package com.topjohnwu.magisk.model.entity;
import android.os.Parcel;
import android.os.Parcelable;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
public class Module extends BaseModule {
private SuFile mRemoveFile, mDisableFile, mUpdateFile;
private boolean mEnable, mRemove, mUpdated;
public Module(String path) {
try {
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
} catch (NumberFormatException ignored) {}
mRemoveFile = new SuFile(path, "remove");
mDisableFile = new SuFile(path, "disable");
mUpdateFile = new SuFile(path, "update");
if (getId().isEmpty()) {
int sep = path.lastIndexOf('/');
setId(path.substring(sep + 1));
}
if (getName().isEmpty()) {
setName(getId());
}
mEnable = !mDisableFile.exists();
mRemove = mRemoveFile.exists();
mUpdated = mUpdateFile.exists();
}
public static final Parcelable.Creator<Module> CREATOR = new Creator<Module>() {
/* It won't be used at any place */
@Override
public Module createFromParcel(Parcel source) {
return null;
}
@Override
public Module[] newArray(int size) {
return null;
}
};
public void createDisableFile() {
mEnable = !mDisableFile.createNewFile();
}
public void removeDisableFile() {
mEnable = mDisableFile.delete();
}
public boolean isEnabled() {
return mEnable;
}
public void createRemoveFile() {
mRemove = mRemoveFile.createNewFile();
}
public void deleteRemoveFile() {
mRemove = !mRemoveFile.delete();
}
public boolean willBeRemoved() {
return mRemove;
}
public boolean isUpdated() {
return mUpdated;
}
}
@@ -0,0 +1,61 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import com.topjohnwu.magisk.utils.Utils;
import androidx.annotation.NonNull;
public class Policy implements Comparable<Policy>{
public static final int INTERACTIVE = 0;
public static final int DENY = 1;
public static final int ALLOW = 2;
public int uid, policy = INTERACTIVE;
public long until;
public boolean logging = true, notification = true;
public String packageName, appName;
public ApplicationInfo info;
public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException {
String[] pkgs = pm.getPackagesForUid(uid);
if (pkgs == null || pkgs.length == 0)
throw new PackageManager.NameNotFoundException();
this.uid = uid;
packageName = pkgs[0];
info = pm.getApplicationInfo(packageName, 0);
appName = Utils.getAppLabel(info, pm);
}
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {
uid = values.getAsInteger("uid");
packageName = values.getAsString("package_name");
policy = values.getAsInteger("policy");
until = values.getAsInteger("until");
logging = values.getAsInteger("logging") != 0;
notification = values.getAsInteger("notification") != 0;
info = pm.getApplicationInfo(packageName, 0);
if (info.uid != uid)
throw new PackageManager.NameNotFoundException();
appName = info.loadLabel(pm).toString();
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("uid", uid);
values.put("package_name", packageName);
values.put("policy", policy);
values.put("until", until);
values.put("logging", logging ? 1 : 0);
values.put("notification", notification ? 1 : 0);
return values;
}
@Override
public int compareTo(@NonNull Policy policy) {
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
}
}
@@ -0,0 +1,123 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.Networking;
import com.topjohnwu.net.Request;
import java.text.DateFormat;
import java.util.Date;
public class Repo extends BaseModule {
private Date mLastUpdate;
public Repo(String id) {
setId(id);
}
public Repo(Cursor c) {
super(c);
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
}
public Repo(Parcel p) {
super(p);
mLastUpdate = new Date(p.readLong());
}
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
@Override
public Repo createFromParcel(Parcel source) {
return new Repo(source);
}
@Override
public Repo[] newArray(int size) {
return new Repo[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(mLastUpdate.getTime());
}
public void update() throws IllegalRepoException {
String props[] = Utils.dlString(getPropUrl()).split("\\n");
try {
parseProps(props);
} catch (NumberFormatException e) {
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
}
if (getVersionCode() < 0) {
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
}
}
public void update(Date lastUpdate) throws IllegalRepoException {
mLastUpdate = lastUpdate;
update();
}
@Override
public ContentValues getContentValues() {
ContentValues values = super.getContentValues();
values.put("last_update", mLastUpdate.getTime());
return values;
}
public String getZipUrl() {
return String.format(Const.Url.ZIP_URL, getId());
}
public String getPropUrl() {
return getFileUrl("module.prop");
}
public String getDetailUrl() {
return getFileUrl("README.md");
}
public String getFileUrl(String file) {
return String.format(Const.Url.FILE_URL, getId(), file);
}
public boolean isNewInstaller() {
try (Request install = Networking.get(getFileUrl("install.sh"))) {
if (install.connect().isSuccess()) {
// Double check whether config.sh exists
try (Request config = Networking.get(getFileUrl("config.sh"))) {
return !config.connect().isSuccess();
}
}
return false;
}
}
public String getLastUpdateString() {
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
}
public Date getLastUpdate() {
return mLastUpdate;
}
public String getDownloadFilename() {
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
}
public class IllegalRepoException extends Exception {
IllegalRepoException(String message) {
super(message);
}
}
}
@@ -0,0 +1,56 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import com.topjohnwu.magisk.utils.LocaleManager;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SuLogEntry {
public int fromUid, toUid, fromPid;
public String packageName, appName, command;
public boolean action;
public Date date;
public SuLogEntry(Policy policy) {
fromUid = policy.uid;
packageName = policy.packageName;
appName = policy.appName;
action = policy.policy == Policy.ALLOW;
}
public SuLogEntry(ContentValues values) {
fromUid = values.getAsInteger("from_uid");
packageName = values.getAsString("package_name");
appName = values.getAsString("app_name");
fromPid = values.getAsInteger("from_pid");
command = values.getAsString("command");
toUid = values.getAsInteger("to_uid");
action = values.getAsInteger("action") != 0;
date = new Date(values.getAsLong("time"));
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("from_uid", fromUid);
values.put("package_name", packageName);
values.put("app_name", appName);
values.put("from_pid", fromPid);
values.put("command", command);
values.put("to_uid", toUid);
values.put("action", action ? 1 : 0);
values.put("time", date.getTime());
return values;
}
public String getDateString() {
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
}
public String getTimeString() {
return new SimpleDateFormat("h:mm a", LocaleManager.locale).format(date);
}
}
@@ -0,0 +1,84 @@
package com.topjohnwu.magisk.model.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
import com.topjohnwu.magisk.utils.DownloadApp;
import com.topjohnwu.magisk.utils.SuLogger;
import com.topjohnwu.magisk.view.Notifications;
import com.topjohnwu.magisk.view.Shortcuts;
import com.topjohnwu.superuser.Shell;
public class GeneralReceiver extends BroadcastReceiver {
private String getPkg(Intent i) {
return i.getData() == null ? "" : i.getData().getEncodedSchemeSpecificPart();
}
@Override
public void onReceive(Context context, Intent intent) {
App app = App.self;
if (intent == null)
return;
String action = intent.getAction();
if (action == null)
return;
switch (action) {
case Intent.ACTION_REBOOT:
case Intent.ACTION_BOOT_COMPLETED:
action = intent.getStringExtra("action");
if (action == null) {
// Actual boot completed event
Shell.su("mm_patch_dtbo").submit(result -> {
if (result.isSuccess())
Notifications.dtboPatched();
});
break;
}
switch (action) {
case SuRequestActivity.REQUEST:
Intent i = new Intent(app, ClassMap.get(SuRequestActivity.class))
.setAction(action)
.putExtra("socket", intent.getStringExtra("socket"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
app.startActivity(i);
break;
case SuRequestActivity.LOG:
SuLogger.handleLogs(intent);
break;
case SuRequestActivity.NOTIFY:
SuLogger.handleNotify(intent);
break;
}
break;
case Intent.ACTION_PACKAGE_REPLACED:
// This will only work pre-O
if (Config.get(Config.Key.SU_REAUTH)) {
app.mDB.deletePolicy(getPkg(intent));
}
break;
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
String pkg = getPkg(intent);
app.mDB.deletePolicy(pkg);
Shell.su("magiskhide --rm " + pkg).submit();
break;
case Intent.ACTION_LOCALE_CHANGED:
Shortcuts.setup(context);
break;
case Const.Key.BROADCAST_MANAGER_UPDATE:
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK);
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME));
break;
case Const.Key.BROADCAST_REBOOT:
Shell.su("/system/bin/reboot").submit();
break;
}
}
}
@@ -0,0 +1,33 @@
package com.topjohnwu.magisk.model.update;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.model.worker.DelegateWorker;
import com.topjohnwu.magisk.tasks.CheckUpdates;
import com.topjohnwu.magisk.view.Notifications;
import com.topjohnwu.superuser.Shell;
import androidx.annotation.NonNull;
import androidx.work.ListenableWorker;
public class UpdateCheckService extends DelegateWorker {
@NonNull
@Override
public ListenableWorker.Result doWork() {
if (App.foreground() == null) {
Shell.getShell();
CheckUpdates.check(this::onCheckDone);
}
return ListenableWorker.Result.success();
}
private void onCheckDone() {
if (BuildConfig.VERSION_CODE < Config.remoteManagerVersionCode) {
Notifications.managerUpdate();
} else if (Config.magiskVersionCode < Config.remoteMagiskVersionCode) {
Notifications.magiskUpdate();
}
}
}
@@ -0,0 +1,84 @@
package com.topjohnwu.magisk.model.worker;
import android.content.Context;
import android.net.Network;
import android.net.Uri;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.work.Data;
import androidx.work.ListenableWorker;
public abstract class DelegateWorker {
private ListenableWorker worker;
@NonNull
public abstract ListenableWorker.Result doWork();
public void onStopped() {}
public void setActualWorker(ListenableWorker w) {
worker = w;
}
@NonNull
public Context getApplicationContext() {
return worker.getApplicationContext();
}
@NonNull
public UUID getId() {
return worker.getId();
}
@NonNull
public Data getInputData() {
return worker.getInputData();
}
@NonNull
public Set<String> getTags() {
return worker.getTags();
}
@NonNull
@RequiresApi(24)
public List<Uri> getTriggeredContentUris() {
return worker.getTriggeredContentUris();
}
@NonNull
@RequiresApi(24)
public List<String> getTriggeredContentAuthorities() {
return worker.getTriggeredContentAuthorities();
}
@Nullable
@RequiresApi(28)
public Network getNetwork() {
return worker.getNetwork();
}
public int getRunAttemptCount() {
return worker.getRunAttemptCount();
}
@NonNull
@MainThread
public ListenableFuture<ListenableWorker.Result> startWork() {
return worker.startWork();
}
public boolean isStopped() {
return worker.isStopped();
}
}