0
0
mirror of https://github.com/TrianguloY/UrlChecker.git synced 2024-09-19 20:02:16 +02:00

Flags module update

This commit is contained in:
Pablo Ortigosa 2023-03-31 19:09:57 +01:00
parent 027351bdf5
commit 020051846d
12 changed files with 837 additions and 260 deletions

View File

@ -0,0 +1,206 @@
package com.trianguloy.urlchecker.modules.companions;
import android.content.Intent;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* Represents intent flags
* Stores all the flags and it states, can be retrieved as list or as int
* Non-compatible flags are ignored
*/
public class Flags {
// ------------------- static -------------------
// https://github.com/MuntashirAkon/AppManager/blob/19782da4c8556c817ba5795554a1cc21f38af13a/app/src/main/java/io/github/muntashirakon/AppManager/intercept/ActivityInterceptor.java#L92
private static final Set<String> ALL_FLAGS = Set.of(
"FLAG_GRANT_READ_URI_PERMISSION",
"FLAG_GRANT_WRITE_URI_PERMISSION",
"FLAG_FROM_BACKGROUND",
"FLAG_DEBUG_LOG_RESOLUTION",
"FLAG_EXCLUDE_STOPPED_PACKAGES",
"FLAG_INCLUDE_STOPPED_PACKAGES",
"FLAG_GRANT_PERSISTABLE_URI_PERMISSION",
"FLAG_GRANT_PREFIX_URI_PERMISSION",
"FLAG_DIRECT_BOOT_AUTO",
"FLAG_IGNORE_EPHEMERAL",
"FLAG_ACTIVITY_NO_HISTORY",
"FLAG_ACTIVITY_SINGLE_TOP",
"FLAG_ACTIVITY_NEW_TASK",
"FLAG_ACTIVITY_MULTIPLE_TASK",
"FLAG_ACTIVITY_CLEAR_TOP",
"FLAG_ACTIVITY_FORWARD_RESULT",
"FLAG_ACTIVITY_PREVIOUS_IS_TOP",
"FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS",
"FLAG_ACTIVITY_BROUGHT_TO_FRONT",
"FLAG_ACTIVITY_RESET_TASK_IF_NEEDED",
"FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY",
"FLAG_ACTIVITY_NEW_DOCUMENT",
"FLAG_ACTIVITY_NO_USER_ACTION",
"FLAG_ACTIVITY_REORDER_TO_FRONT",
"FLAG_ACTIVITY_NO_ANIMATION",
"FLAG_ACTIVITY_CLEAR_TASK",
"FLAG_ACTIVITY_TASK_ON_HOME",
"FLAG_ACTIVITY_RETAIN_IN_RECENTS",
"FLAG_ACTIVITY_LAUNCH_ADJACENT",
"FLAG_ACTIVITY_MATCH_EXTERNAL",
"FLAG_ACTIVITY_REQUIRE_NON_BROWSER",
"FLAG_ACTIVITY_REQUIRE_DEFAULT"
);
private static final Map<String, Integer> compatibleFlags = new TreeMap<>(); // TreeMap to have the entries sorted by key
static {
try {
// Only get flags that are present in the current Android version
for (var field : Intent.class.getFields()) {
if (ALL_FLAGS.contains(field.getName())) {
compatibleFlags.put(field.getName(), (Integer) field.get(null));
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static Map<String, Integer> getCompatibleFlags(){
return new TreeMap<>(compatibleFlags);
}
// ------------------- CRUD -------------------
private Set<String> flags;
// TODO store non-compatible flags? as String "0x001000", int 0x001000
public Flags(){
this(0x00000000);
}
public Flags(int hex){
setFlags(hex);
}
public boolean isSet(String flag){
return flags.contains(flag);
}
public int getFlagsAsInt(){
return flagsSetToHex(flags);
}
public Set<String> getFlagsAsSet() {
return new TreeSet<>(flags); // TreeSet to have the entries sorted
}
/**
* Replaces the stored flags with the received hex
*/
public void setFlags(int hex){
this.flags = hexFlagsToSet(hex);
}
/**
* Replaces the stored flags with the received list
*/
public void setFlags(Set<String> names){
Set<String> res = new HashSet<>(names);
res.retainAll(compatibleFlags.keySet());
this.flags = res;
}
/**
* Sets the flag to the received boolean
*/
public boolean setFlag(String flag, boolean bool){
if (compatibleFlags.containsKey(flag)){
if (bool){
return flags.add(flag);
} else {
return flags.remove(flag);
}
} else {
return false;
}
}
/**
* Add flags by applying a mask
*/
public void addFlags(int hex){
flags.addAll(hexFlagsToSet(hex));
}
/**
* Add a list of flags based on its name
*/
public void addFlags(Set<String> flags){
this.flags.addAll(flags);
}
/**
* Add a flag based on its name
*/
public boolean addFlag(String name) {
if (compatibleFlags.containsKey(name)) {
return flags.add(name);
} else {
return false;
}
}
/**
* Remove flags by applying a mask
*/
public boolean removeFlags(int hex){
return flags.removeAll(hexFlagsToSet(hex));
}
/**
* Remove a list of flags based on its name
*/
public boolean removeFlags(Set<String> flags){
return this.flags.removeAll(flags);
}
/**
* Remove a flag based on its name
*/
public boolean removeFlag(String name){
return flags.remove(name);
}
// ------------------- utils -------------------
/**
* Decode an int as set
*/
private static Set<String> hexFlagsToSet(int hex) {
var foundFlags = new HashSet<String>();
for (var flag : compatibleFlags.entrySet()) {
// check if flag is present
if ((hex & flag.getValue()) != 0) {
foundFlags.add(flag.getKey());
}
}
return foundFlags;
}
/**
* Decode a set as hex
*/
private static int flagsSetToHex(Set<String> set) {
int hex = 0x00000000;
for (var flag : set){
if (compatibleFlags.containsKey(flag)) {
hex = hex | compatibleFlags.get(flag);
}
}
return hex;
}
}

View File

@ -1,14 +1,19 @@
package com.trianguloy.urlchecker.modules.list;
import static com.trianguloy.urlchecker.utilities.JavaUtils.valueOrDefault;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
@ -18,15 +23,25 @@ import com.trianguloy.urlchecker.dialogs.MainDialog;
import com.trianguloy.urlchecker.modules.AModuleConfig;
import com.trianguloy.urlchecker.modules.AModuleData;
import com.trianguloy.urlchecker.modules.AModuleDialog;
import com.trianguloy.urlchecker.modules.companions.Flags;
import com.trianguloy.urlchecker.url.UrlData;
import com.trianguloy.urlchecker.utilities.AndroidUtils;
import com.trianguloy.urlchecker.utilities.GenericPref;
import com.trianguloy.urlchecker.utilities.Inflater;
import com.trianguloy.urlchecker.utilities.InternalFile;
import com.trianguloy.urlchecker.utilities.JavaUtils;
import com.trianguloy.urlchecker.utilities.TranslatableEnum;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Set;
import java.util.TreeSet;
/**
* This module allows flag edition
@ -67,196 +82,240 @@ class FlagsDialog extends AModuleDialog {
public static final String DATA_FLAGS = "flagsEditor.flags";
// https://github.com/MuntashirAkon/AppManager/blob/19782da4c8556c817ba5795554a1cc21f38af13a/app/src/main/java/io/github/muntashirakon/AppManager/intercept/ActivityInterceptor.java#L92
private static final List<String> ALL_FLAGS = List.of(
"FLAG_GRANT_READ_URI_PERMISSION",
"FLAG_GRANT_WRITE_URI_PERMISSION",
"FLAG_FROM_BACKGROUND",
"FLAG_DEBUG_LOG_RESOLUTION",
"FLAG_EXCLUDE_STOPPED_PACKAGES",
"FLAG_INCLUDE_STOPPED_PACKAGES",
"FLAG_GRANT_PERSISTABLE_URI_PERMISSION",
"FLAG_GRANT_PREFIX_URI_PERMISSION",
"FLAG_DIRECT_BOOT_AUTO",
"FLAG_IGNORE_EPHEMERAL",
"FLAG_ACTIVITY_NO_HISTORY",
"FLAG_ACTIVITY_SINGLE_TOP",
"FLAG_ACTIVITY_NEW_TASK",
"FLAG_ACTIVITY_MULTIPLE_TASK",
"FLAG_ACTIVITY_CLEAR_TOP",
"FLAG_ACTIVITY_FORWARD_RESULT",
"FLAG_ACTIVITY_PREVIOUS_IS_TOP",
"FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS",
"FLAG_ACTIVITY_BROUGHT_TO_FRONT",
"FLAG_ACTIVITY_RESET_TASK_IF_NEEDED",
"FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY",
"FLAG_ACTIVITY_NEW_DOCUMENT",
"FLAG_ACTIVITY_NO_USER_ACTION",
"FLAG_ACTIVITY_REORDER_TO_FRONT",
"FLAG_ACTIVITY_NO_ANIMATION",
"FLAG_ACTIVITY_CLEAR_TASK",
"FLAG_ACTIVITY_TASK_ON_HOME",
"FLAG_ACTIVITY_RETAIN_IN_RECENTS",
"FLAG_ACTIVITY_LAUNCH_ADJACENT",
"FLAG_ACTIVITY_MATCH_EXTERNAL",
"FLAG_ACTIVITY_REQUIRE_NON_BROWSER",
"FLAG_ACTIVITY_REQUIRE_DEFAULT"
);
private final Map<String, Integer> flagMap = new TreeMap<>(); // TreeMap to have the entries sorted by key
private final Flags defaultFlags;
private final Flags currentFlags;
private final GenericPref.Str defaultFlagsPref;
private Map<String, FlagsConfig.FlagState> flagsStatePref;
private EditText flagsHexText;
private AutoCompleteTextView flagNameText;
private ImageButton more;
private LinearLayout box;
private ViewGroup shownFlagsVG;
private ViewGroup hiddenFlagsAndSearchVG;
private ViewGroup hiddenFlagsVG;
private ImageView overflowButton;
private JSONObject groups;
public FlagsDialog(MainDialog dialog) {
super(dialog);
defaultFlagsPref = FlagsModule.DEFAULTFLAGS_PREF(dialog);
try {
// Only get flags that are present in the current Android version
for (var field : Intent.class.getFields()) {
if (ALL_FLAGS.contains(field.getName())) {
flagMap.put(field.getName(), (Integer) field.get(null));
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
defaultFlags = new Flags(getActivity().getIntent().getFlags());
currentFlags = new Flags();
}
@Override
public int getLayoutId() {
return R.layout.dialog_editflags;
return R.layout.dialog_flagseditor;
}
@Override
public void onInitialize(View views) {
box = views.findViewById(R.id.box);
flagsHexText = views.findViewById(R.id.flagsHexText);
initGroups();
// set the flags to the adapter of the input text
flagNameText = views.findViewById(R.id.flagText);
flagNameText.setAdapter(new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_dropdown_item, new ArrayList<>(
flagMap.keySet()
)));
// so the dropdown gets the maximum width possible
flagNameText.setDropDownAnchor(R.id.addFlagLayout);
// FIXME better search, currently it is an autofill, not a search
// FIXME sometimes its hidden behind keyboard
shownFlagsVG = views.findViewById(R.id.shownFlags);
hiddenFlagsAndSearchVG = views.findViewById(R.id.hiddenFlagsAndSearch);
hiddenFlagsVG = views.findViewById(R.id.hiddenFlags);
// get initial flags
var defaultFlagsStr = defaultFlagsPref.get();
if (defaultFlagsStr != null) {
setFlags(toInteger(defaultFlagsStr));
}
// Button to open the `box` with the hidden flags (more indicator)
overflowButton = views.findViewById(R.id.overflowButton);
overflowButton.setOnClickListener(v -> {
hiddenFlagsAndSearchVG.setVisibility(
hiddenFlagsAndSearchVG.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
updateMoreIndicator();
});
// press add to add a flag
views.<Button>findViewById(R.id.add).setOnClickListener(v -> {
var flag = flagMap.get(flagNameText.getText().toString());
if (flag != null) {
setFlags(getFlagsNonNull() | flag);
} else {
Toast.makeText(getActivity(), R.string.mFlags_invalid, Toast.LENGTH_LONG).show();
// Hide hidden flags
hiddenFlagsAndSearchVG.setVisibility(View.GONE);
// SEARCH
// Set up search text
((EditText) views.findViewById(R.id.search)).addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
// Update views
updateLayout();
});
// press 'more' to expand/collapse the box
more = views.findViewById(R.id.more);
more.setOnClickListener(v -> {
box.setVisibility(box.getVisibility() == View.GONE ? View.VISIBLE : View.GONE);
// Update views
updateLayout();
});
box.setVisibility(View.GONE); // start collapsed
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
// press edit to start editing, press again to save
var edit = views.<Button>findViewById(R.id.edit);
edit.setOnClickListener(v -> {
if (flagsHexText.isEnabled()) {
// requested to save
var flags = toInteger(flagsHexText.getText().toString());
if (flags != null) {
// Extract flags
setFlags(flags);
@Override
public void afterTextChanged(Editable text) {
for (int i = 0; i < hiddenFlagsVG.getChildCount(); i++) {
var checkbox_text = hiddenFlagsVG.getChildAt(i);
String flag = ((TextView) checkbox_text.findViewById(R.id.text)).getText().toString();
String search = text.toString();
// Set visibility based on search text
checkbox_text.setVisibility(
JavaUtils.containsWords(flag, search) ? View.VISIBLE : View.GONE);
}
}
flagsHexText.setEnabled(!flagsHexText.isEnabled());
// Update views
updateLayout();
});
// long press to reset
edit.setOnLongClickListener(v -> {
// Resets the flags
setFlags(null);
// Update views
updateLayout();
return true;
});
// TODO spinner with groups
loadGroup("default");
}
@Override
public void onDisplayUrl(UrlData urlData) {
updateLayout();
}
private void updateLayout() {
// set text
var flags = getFlagsNonNull();
flagsHexText.setText(toHexString(flags));
// set current flags list
var decodedFlags = decodeFlags(flags);
box.removeAllViews();
if (decodedFlags.size() == 0) {
// no flags, disable
AndroidUtils.setEnabled(more, false);
more.setImageResource(R.drawable.arrow_right);
} else {
// flags, enable
AndroidUtils.setEnabled(more, true);
more.setImageResource(box.getVisibility() == View.VISIBLE ? R.drawable.arrow_down : R.drawable.arrow_right);
// For each flag, create a button+text
for (var flag : decodedFlags) {
var button_text = Inflater.inflate(R.layout.button_text, box, getActivity());
// Button that removes the flag
var button = button_text.<Button>findViewById(R.id.button);
button.setText(R.string.remove);
// Apply mask to remove flag
button.setOnClickListener(v -> {
setFlags(getFlagsNonNull() & ~flagMap.get(flag));
// Update views
updateLayout();
});
var text = button_text.<TextView>findViewById(R.id.text);
text.setText(flag);
private void initGroups(){
String fileString = new InternalFile(FlagsConfig.CONF_FILE, getActivity()).get();
groups = null;
if (fileString != null){
try {
groups = new JSONObject(fileString).getJSONObject("groups");
} catch (JSONException ignore) {
}
}
}
// ------------------- utils -------------------
// To get all the groups names
private List<String> getGroups(){
List<String> res = new ArrayList<>();
// Always add "default" first, even if it doesn't exist
res.add("default");
for (Iterator<String> it = groups.keys(); it.hasNext(); ) {
String group = it.next();
if (!group.equals("default")){
res.add(group);
}
}
return res;
}
void loadGroup(String group){
currentFlags.setFlags(0);
// Load json
JSONObject groupPref = null;
try {
groupPref = groups.getJSONObject(group);
} catch (JSONException ignore) {
}
// STATE
// Get state preference of flag from json and then store it in a map
flagsStatePref = new HashMap<>();
if (groupPref != null) {
try {
Map<Integer, FlagsConfig.FlagState> flagsStateMap = TranslatableEnum.toEnumMap(FlagsConfig.FlagState.class);
for (Iterator<String> it = groupPref.keys(); it.hasNext(); ) {
String flag = it.next();
flagsStatePref.put(flag, flagsStateMap.get(groupPref.getJSONObject(flag).getInt("state")));
}
} catch (JSONException ignored) {
}
}
// SHOW
// Put shown flags
Set<String> shownFlagsSet = new TreeSet<>();
if (groupPref != null) {
try {
for (Iterator<String> it = groupPref.keys(); it.hasNext(); ) {
String flag = it.next();
if (groupPref.getJSONObject(flag).getBoolean("show")) {
shownFlagsSet.add(flag);
}
}
} catch (JSONException ignored) {
}
}
// If it is not in shownFlags it must be in hiddenFlags
Set<String> hiddenFlagsSet = new TreeSet<>(Flags.getCompatibleFlags().keySet());
hiddenFlagsSet.removeAll(shownFlagsSet);
// Fill boxes with flags, load flags into currentFlags too
fillWithFlags(shownFlagsSet, shownFlagsVG);
fillWithFlags(hiddenFlagsSet, hiddenFlagsVG);
// Update global
setGlobalFlags(currentFlags);
updateMoreIndicator();
}
void updateMoreIndicator(){
overflowButton.setImageResource(hiddenFlagsVG.getChildCount() == 0 ? 0
: hiddenFlagsAndSearchVG.getVisibility() == View.VISIBLE ? R.drawable.arrow_down
: R.drawable.arrow_right);
}
/**
* Decode an int as flags
* Sets up a ViewGroup with the flags received. The state of the flag is read from flagsStatePref.
* The default state of the flag is read from defaultFlags. Also sets currentFlags.
*
* @param flags flags to add
* @param vg ViewGroup to fill with flags
*/
private List<String> decodeFlags(int hex) {
var foundFlags = new ArrayList<String>();
for (var flag : flagMap.entrySet()) {
// check if flag is present
if ((hex & flag.getValue()) != 0) {
foundFlags.add(flag.getKey());
private void fillWithFlags(Set<String> flags, ViewGroup vg){
vg.removeAllViews();
// Checkbox listener
CompoundButton.OnCheckedChangeListener l = (v, isChecked) -> {
// Store flag
String flag = (String) v.getTag(R.id.text);
currentFlags.setFlag(flag, isChecked);
// Update global
setGlobalFlags(currentFlags);
// To update debug module view of GlobalData
setUrl(new UrlData(getUrl()).dontTriggerOwn().asMinorUpdate());
};
for (String flag : flags) {
var checkbox_text = Inflater.inflate(R.layout.checkbox_text, vg, getActivity());
// Checkbox
CheckBox checkBox = checkbox_text.findViewById(R.id.checkbox);
boolean bool;
switch (valueOrDefault(flagsStatePref.get(flag), FlagsConfig.FlagState.AUTO)){
case ON:
bool = true;
break;
case OFF:
bool = false;
break;
case AUTO:
default:
bool = defaultFlags.isSet(flag);
}
checkBox.setChecked(bool);
currentFlags.setFlag(flag ,bool);
checkBox.setTag(R.id.text, flag);
checkBox.setOnCheckedChangeListener(l);
// Text
((TextView) checkbox_text.findViewById(R.id.text)).setText(flag);
// Color indicators
var defaultIndicator = checkbox_text.findViewById(R.id.defaultIndicator);
var preferenceIndicator = checkbox_text.findViewById(R.id.preferenceIndicator);
checkBox.setTag(R.id.defaultIndicator, defaultIndicator);
checkBox.setTag(R.id.preferenceIndicator, preferenceIndicator);
setColors(flag, defaultIndicator, preferenceIndicator);
}
return foundFlags;
}
void setColors(String flag, View defaultIndicator, View preferenceIndicator){
AndroidUtils.setRoundedColor(defaultFlags.isSet(flag) ? R.color.good : R.color.bad , defaultIndicator);
int color;
switch (valueOrDefault(flagsStatePref.get(flag), FlagsConfig.FlagState.AUTO)){
case ON:
color = R.color.good;
break;
case OFF:
color = R.color.bad;
break;
case AUTO:
default:
color = R.color.grey;
}
AndroidUtils.setRoundedColor(color, preferenceIndicator);
}
// ------------------- store/load flags -------------------
@ -289,7 +348,7 @@ class FlagsDialog extends AModuleDialog {
* Retrieves the flags from GlobalData, if it is not defined it will return null
* Intended for use in other modules
*/
public static Integer getFlagsNullable(AModuleDialog instance) {
public static Integer getGlobalFlagsNullable(AModuleDialog instance) {
return toInteger(instance.getData(DATA_FLAGS));
}
@ -297,34 +356,34 @@ class FlagsDialog extends AModuleDialog {
* Loads the flags from GlobalData, if none were found it gets the flags from the intent that
* started this activity
*/
private int getFlagsNonNull() {
return getFlagsOrDefault(this, getActivity().getIntent().getFlags());
private int getGlobalFlagsNonNull() {
return getGlobalFlagsOrDefault(this, getActivity().getIntent().getFlags());
}
/**
* Loads the flags from GlobalData, if none were found it gets the flags from default
* Can be used by other modules
*/
public static int getFlagsOrDefault(AModuleDialog instance, int defaultFlags) {
var flags = toInteger(instance.getData(DATA_FLAGS));
return flags == null ? defaultFlags : flags;
public static int getGlobalFlagsOrDefault(AModuleDialog instance, int defaultFlags) {
return valueOrDefault(toInteger(instance.getData(DATA_FLAGS)), defaultFlags);
}
/**
* Stores the flags in GlobalData
*/
private void setFlags(Integer flags) {
putData(DATA_FLAGS, flags == null ? null : toHexString(flags));
private void setGlobalFlags(Flags flags) {
putData(DATA_FLAGS, flags == null ? null : toHexString(flags.getFlagsAsInt()));
}
}
class FlagsConfig extends AModuleConfig {
private final GenericPref.Str defaultFlagsPref;
protected static final String CONF_FILE = "flags_editor_settings";
private Map<Integer, Integer> stateToIndex;
public FlagsConfig(ModulesActivity activity) {
super(activity);
defaultFlagsPref = FlagsModule.DEFAULTFLAGS_PREF(activity);
}
@Override
@ -334,10 +393,200 @@ class FlagsConfig extends AModuleConfig {
@Override
public void onInitialize(View views) {
defaultFlagsPref.attachToEditText(
views.findViewById(R.id.flags),
str -> str,
str -> str.matches(FlagsDialog.REGEX) ? str : defaultFlagsPref.defaultValue
views.findViewById(R.id.button).setOnClickListener(showDialog -> {
View flagsDialogLayout = getActivity().getLayoutInflater().inflate(R.layout.flagseditor_groupeditor, null);
ViewGroup box = flagsDialogLayout.findViewById(R.id.box);
InternalFile file = new InternalFile(CONF_FILE, flagsDialogLayout.getContext());
// Get all flags
fillBoxViewGroup(box, file, "default");
AlertDialog alertDialog = new AlertDialog.Builder(getActivity())
.setView(flagsDialogLayout)
.setPositiveButton(views.getContext().getText(R.string.save), null)
.setNegativeButton(views.getContext().getText(android.R.string.cancel), null)
.setNeutralButton(views.getContext().getText(R.string.reset), null)
.show();
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(listener -> {
// Save the settings
storePreferences(box, file, "default");
});
alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(listener -> {
// Reset current group flags (does not save)
resetFlags(box);
});
// Search
((EditText) flagsDialogLayout.findViewById(R.id.search)).addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable text) {
for (int i = 0; i < box.getChildCount(); i++) {
var text_spinner_checkbox = box.getChildAt(i);
String flag = ((TextView) text_spinner_checkbox.findViewById(R.id.text)).getText().toString();
String search = text.toString();
// Set visibility based on search text
text_spinner_checkbox.setVisibility(
JavaUtils.containsWords(flag, search) ? View.VISIBLE : View.GONE);
}
}
});
// TODO add dialog button to set all to on/off/auto
});
}
// FIXME spinner gfx bug
private void fillBoxViewGroup(ViewGroup vg, InternalFile file, String group){
// Set spinner items
FlagState[] spinnerItems = FlagState.class.getEnumConstants();
List<String> spinnerItemsList = new ArrayList<>(spinnerItems.length);
stateToIndex = new HashMap<>();
for (int i = 0; i < spinnerItems.length; i++) {
spinnerItemsList.add(vg.getContext().getString(spinnerItems[i].getStringResource()));
// Map state to index
stateToIndex.put(spinnerItems[i].getId(), i);
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(
vg.getContext(),
android.R.layout.simple_spinner_item,
spinnerItemsList
);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// Store order info in vg
vg.setTag(spinnerItems);
String prefString = file.get();
JSONObject oldPref = null; // Null if there is no file or fails to parse
try {
oldPref = prefString == null ? null : new JSONObject(file.get()).getJSONObject("groups").getJSONObject(group);
} catch (JSONException ignored) {
}
// Fill the box
for (String flag : Flags.getCompatibleFlags().keySet()) {
var text_spinner_checkbox = Inflater.inflate(R.layout.text_spinner_checkbox, vg, getActivity());
TextView textView = text_spinner_checkbox.findViewById(R.id.text);
textView.setText(flag);
Spinner spinner = text_spinner_checkbox.findViewById(R.id.spinner);
spinner.setAdapter(adapter);
spinner.setTag(spinnerItems);
// Load preferences from settings
if (oldPref != null) {
JSONObject flagPref;
try {
flagPref = oldPref.getJSONObject(flag);
// select current option
spinner.setSelection(valueOrDefault(stateToIndex.get(flagPref.getInt("state")),
FlagState.AUTO.getId()));
((CheckBox) text_spinner_checkbox.findViewById(R.id.checkbox)).setChecked(flagPref.getBoolean("show"));
} catch (JSONException ignored) {
}
}
}
}
private void storePreferences(ViewGroup vg, InternalFile file, String group){
// Retrieve previous config, to keep other groups
JSONObject oldSettings = null;
String content = file.get();
// It's ok if there is no file yet
if (content != null){
try {
oldSettings = new JSONObject(content);
} catch (JSONException ignore) {
// If the json fails to parse then we will create a new file
}
}
// Retrieve order of spinner
FlagState[] spinnerItems = (FlagState[]) vg.getTag();
try {
// Collect all the settings of the vg
JSONObject newSettings = new JSONObject();
for (int i = 0; i < vg.getChildCount(); i++) {
View v = vg.getChildAt(i);
FlagState state = spinnerItems[((Spinner) v.findViewById(R.id.spinner)).getSelectedItemPosition()];
boolean show = ((CheckBox) v.findViewById(R.id.checkbox)).isChecked();
newSettings.put(((TextView) v.findViewById(R.id.text)).getText().toString(),
new JSONObject()
.put("state", state.getId())
.put("show", show));
}
// If there are no old settings, create a new one
// Replace the old settings from group with the new ones
newSettings = oldSettings == null ?
new JSONObject().put("groups", new JSONObject().put(group, newSettings)) :
oldSettings.put("groups", oldSettings.getJSONObject("groups").put(group, newSettings));
// TODO should groups be sorted?
file.set(newSettings.toString());
} catch (JSONException e) {
e.printStackTrace();
Toast.makeText(getActivity(), R.string.toast_invalid, Toast.LENGTH_SHORT).show();
}
}
private void resetFlags(ViewGroup vg){
// Retrieve order of spinner
FlagState[] spinnerItems = (FlagState[]) vg.getTag();
// Index of default
int def;
for (def = 0; def < spinnerItems.length; def++){
if (spinnerItems[def] == FlagState.AUTO){
break;
}
}
// Set everything to default values
for (int i = 0; i < vg.getChildCount(); i++) {
View v = vg.getChildAt(i);
((Spinner) v.findViewById(R.id.spinner)).setSelection(def);
((CheckBox) v.findViewById(R.id.checkbox)).setChecked(false);
}
}
public enum FlagState implements TranslatableEnum {
AUTO(0, R.string.auto),
ON(1, R.string.on),
OFF(2, R.string.off),
;
// -----
private final int id;
private final int stringResource;
FlagState(int id, int stringResource) {
this.id = id;
this.stringResource = stringResource;
}
@Override
public int getId() {
return id;
}
@Override
public int getStringResource() {
return stringResource;
}
}
}

View File

@ -251,7 +251,7 @@ class OpenDialog extends AModuleDialog {
}
// Get flags from global data (probably set by flags module, if active)
Integer flags = FlagsDialog.getFlagsNullable(this);
Integer flags = FlagsDialog.getGlobalFlagsNullable(this);
if (flags != null) {
intent.setFlags(flags);
}

View File

@ -45,6 +45,33 @@ public interface JavaUtils {
: clamp(max, value, min);
}
/**
* Applies a filter to both strings to check if all words of keywords are in body.
* The order does not matter.
*/
static boolean containsWords(String body, String keywords){
JavaUtils.Function<String, String> filter = s -> s.toUpperCase().replaceAll("[\\s-_]+", " ");
// Match all words
String[] words = filter.apply(keywords).split(" ");
body = filter.apply(body);
boolean match = true;
for (String str : words) {
if (!body.contains(str)){
match = false;
break;
}
}
return match;
}
/**
* Returns the object, or default if null
* java.util.Optional requires api 24
*/
static <T> T valueOrDefault(T value, T defaultValue){
return value == null ? defaultValue : value;
}
/**
* java.util.function.Consumer requires api 24
*/

View File

@ -1,5 +1,8 @@
package com.trianguloy.urlchecker.utilities;
import java.util.HashMap;
import java.util.Map;
/**
* On java, an enum can't be extended :(
* This is the best I can do...unless I discover how to create annotations!
@ -14,4 +17,29 @@ public interface TranslatableEnum {
* This must return the string resourced associated with this enum value
*/
int getStringResource();
/**
* Get an enum from an id
*/
static <TE extends TranslatableEnum> TE toEnum(Class<TE> te, int id) {
TE[] enumConstants = te.getEnumConstants();
for (TE constant : enumConstants) {
if (constant.getId() == id){
return constant;
}
}
return null;
}
/**
* Get a map of id and enum
*/
static <TE extends TranslatableEnum> Map<Integer, TE> toEnumMap(Class<TE> te) {
Map<Integer, TE> res = new HashMap<>();
TE[] enumConstants = te.getEnumConstants();
for (TE constant : enumConstants) {
res.put(constant.getId(), constant);
}
return res;
}
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0" />
<TextView
android:id="@+id/defaultIndicator"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="3dp"
android:layout_weight="0" />
<TextView
android:id="@+id/preferenceIndicator"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="3dp"
android:layout_weight="0" />
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical" />
</LinearLayout>

View File

@ -10,11 +10,11 @@
android:layout_height="wrap_content"
android:text="@string/mFlag_desc" />
<EditText
android:id="@+id/flags"
<Button
android:id="@+id/button"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="0xFFFFFFFF"
android:inputType="textPersonName" />
android:text="@string/mFlag_editFlags" />
</LinearLayout>

View File

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:id="@+id/flagHexLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/edit"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:text="@string/edit" />
<EditText
android:id="@+id/flagsHexText"
style="@android:style/Widget.TextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:enabled="false"
android:inputType="textPersonName"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/addFlagLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/add"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:text="@string/add" />
<AutoCompleteTextView
android:id="@+id/flagText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ems="10"
android:hint="@string/mFlag_flagHint" />
<ImageButton
android:id="@+id/more"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:src="@drawable/arrow_down"
android:tint="?attr/colorAccent" />
</LinearLayout>
<LinearLayout
android:id="@+id/box"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,52 @@
<?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:layout_gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:id="@+id/shownFlags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="right|center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/overflowButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/smallPadding"
android:layout_weight="0"
android:src="@drawable/arrow_down"
android:tint="?attr/colorAccent" />
</LinearLayout>
<LinearLayout
android:id="@+id/hiddenFlagsAndSearch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@android:string/search_go" />
<LinearLayout
android:id="@+id/hiddenFlags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@android:string/search_go"
android:inputType="textPersonName" />
<LinearLayout
android:id="@+id/box"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"></LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="left|center_vertical" />
<Spinner
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="right|center_vertical" />
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:gravity="center" />
</LinearLayout>

View File

@ -100,6 +100,9 @@ Hope you find the app useful! And don't hesitate to suggest features, report bug
<string name="remove">Remove</string>
<string name="edit">Edit</string>
<string name="add">Add</string>
<string name="on">Enabled</string>
<string name="off">Disabled</string>
<!--
json
-->
@ -303,8 +306,10 @@ Their api is rate limited to 10 requests per hour for new checks. The module res
<string name="mFlags_name">Flags editor</string>
<string name="mFlags_invalid">Invalid flag</string>
<string name="mFlag_desc">"[Beta feature] This is an advanced module, it allows you to edit the intent flags when opening another app.
In the field below you can put flags that will overwrite the default ones, those are taken from the intent that was used to open this app.
While using the module you can hold the edit button to set the flags to the default flags. You can add flags by writing their name in the flags field"</string>
\nThis module will show a list of flags, you can change if the flag is set or not by taping the checkbox. There is also an overflow menu to hide flags in there, to avoid cluttering.
\nThe flags will have two color indicators next to the checkbox. The left one shows the value received from the intent, the right one indicates the state set by the module itself. Red means the flag is not set and green means the flag is set. Additionally the preference indicator can be grey, this means that the state was set to auto.
\nWith the button below you can edit the default state of the flags."</string>
<string name="mFlag_flagHint">Flag name</string>
<string name="mFlag_editFlags">Edit default flag values</string>
</resources>