add annotation processor

main
Jonah 2 years ago
parent ce7330977a
commit 2a267d9d25

@ -0,0 +1,60 @@
package eu.jonahbauer.android.preference.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Defines a preference. The preference key is the value of the string resource
* <pre>{@code R.string.${preference_group_prefix}${name}${preference_group_suffix}}</pre>
* with {@code ${preference_group_prefix}} and {@code ${preference_group_suffix}} being the
* {@linkplain PreferenceGroup#prefix() prefix} and {@linkplain PreferenceGroup#suffix() suffix} defined
* in the enclosing {@link PreferenceGroup}-Annotation.
* <br>
* For each preference with a {@link #type()} other than {@code void.class} a getter
* <pre>{@code public ${type} ${name}()}</pre>
* and a setter
* <pre>{@code public void ${name}(${type} value)}</pre>
* will be generated in the preference group. Additionally, a key accessor
* <pre>{@code public String ${name}()}</pre>
* is generated in the {@code Keys} class of the preference group for each preference (including {@code void}).
* @see Preferences
* @see PreferenceGroup
*/
@Target({})
@Retention(RetentionPolicy.CLASS)
public @interface Preference {
String NO_DEFAULT_VALUE = "__NO_DEFAULT_VALUE__";
/**
* The name of the preference. Must be a valid Java identifier on its own and when combined with
* the preference gropus {@link PreferenceGroup#prefix()} and {@link PreferenceGroup#suffix()}.
*/
String name();
/**
* The type of the preference. Must be one of {@code byte.class}, {@code char.class}, {@code short.class},
* {@code int.class}, {@code long.class}, {@code float.class}, {@code double.class}, {@code boolean.class}
* {@code String.class} or {@code void.class}.
*/
Class<?> type();
/**
* The default value for the preference. If no default value is provided the default value will be
* <ul>
* <li>{@code false} for {@code boolean} preferences,</li>
* <li>{@code 0} for {@code byte}, {@code short}, {@code char}, {@code int}, {@code long}, {@code float} and {@code double} preferences and</li>
* <li>{@code null} for {@code String} preferences</li>
* </ul>
* This field does not have an effect for {@code void} preferences. If the {@link #type()} is {@code String}, then
* the default value is automatically escaped and quoted, otherwise it will be copied into the generated class
* source code as is.
* @implNote it is possibly to inject code into the generated classes by misusing this field. Just don't.
*/
String defaultValue() default NO_DEFAULT_VALUE;
/**
* A description that will be used as documentation for the preference accessors in the generated class.
*/
String description() default "";
}

@ -0,0 +1,40 @@
package eu.jonahbauer.android.preference.annotations;
import java.lang.annotation.*;
/**
* Defines a group of preferences. Every preference group has a name and may contain
* multiple preferences.
* <br>
* For each preference group a {@code static} inner class
* <pre>{@code public static final class ${name} {...}}</pre>
* and an accessor
* <pre>{@code public static ${name} ${name}()}</pre>
* will be generated in the preferences class.
* @see Preferences
* @see Preference
*/
@Target({})
@Retention(RetentionPolicy.CLASS)
public @interface PreferenceGroup {
/**
* A prefix that is prepended to the {@linkplain Preference#name() preference name} in order to build the preference key.
*/
String prefix() default "";
/**
* A suffix that is appended to the {@linkplain Preference#name() preference name} in order to build the preference key.
*/
String suffix() default "";
/**
* The name of the preference group. This is only used as a class name and does not contribute to the preference key.
* Must be a valid Java identifier.
*/
String name();
/**
* A list of {@link Preference}s.
*/
Preference[] value();
}

@ -0,0 +1,117 @@
package eu.jonahbauer.android.preference.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Defines a set of preferences. A preference class for easier and type-safe access to {@code SharedPreferences}
* is generated from this annotation.
* <br>
* <h2>Example</h2>
* <strong>Source Code</strong>
* <pre>{@code @Preferences(name = "org.example.AppPreferences$Generated", r = R.class, value = {
* @PreferenceGroup(name = "general", prefix = "preferences_general_", suffix = "_key", value = {
* @Preference(type = byte.class, name = "byte_pref"),
* @Preference(type = short.class, name = "short_pref"),
* @Preference(type = char.class, name = "char_pref"),
* @Preference(type = int.class, name = "int_pref"),
* @Preference(type = long.class, name = "long_pref"),
* @Preference(type = float.class, name = "float_pref"),
* @Preference(type = double.class, name = "double_pref"),
* @Preference(type = String.class, name = "string_pref"),
* @Preference(type = void.class, name = "void_pref")
* }
* }
* public final AppPreferences extends AppPreferences$Generated {}
* }</pre>
* <strong>String Resources</strong>
* <pre>{@code <resources>
* <string name="preferences_general_byte_pref_key">...</string>
* <string name="preferences_general_short_pref_key">...</string>
* <string name="preferences_general_char_pref_key">...</string>
* <string name="preferences_general_int_pref_key">...</string>
* <string name="preferences_general_long_pref_key">...</string>
* <string name="preferences_general_float_pref_key">...</string>
* <string name="preferences_general_double_pref_key">...</string>
* <string name="preferences_general_string_pref_key">...</string>
* </resources>
* }</pre>
* <strong>Generated Code</strong>
* <pre>{@code
* class AppPreferences$Generated {
* protected AppPreferences$Generated {...} // throws exception
*
* public static void init(SharedPreferences sharedPreferences, Resources resources) {...}
*
* public static general general() {...}
*
* public static final class general {
* private general(Resources resources) {} // private constructor
*
* public Keys keys() {}
*
* public byte bytePref() {}
* public short shortPref() {}
* public char charPref() {}
* public int intPref() {}
* public long longPref() {}
* public float floatPref() {}
* public double doublePref() {}
* public String stringPref() {}
*
* public void bytePref(byte value) {}
* public void shortPref(short value) {}
* public void charPref(char value) {}
* public void intPref(int value) {}
* public void longPref(long value) {}
* public void floatPref(float value) {}
* public void doublePref(double value) {}
* public void stringPref(String value) {}
*
* public final class Keys {
* private Keys() {} // private constructor
* public String bytePref() {}
* public String shortPref() {}
* public String charPref() {}
* public String intPref() {}
* public String longPref() {}
* public String floatPref() {}
* public String doublePref() {}
* public String stringPref() {}
* }
* }
* }
* }</pre>
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Preferences {
/**
* The fully qualified class name of the generated preferences class.
*/
String name();
/**
* A list of {@link PreferenceGroup}s.
*/
PreferenceGroup[] value();
/**
* The type of the app's {@code R} class.
*/
Class<?> r();
/**
* Whether the generated class should be {@code public final} with a {@code private} constructor,
* or package-private non-{@code final} with a {@code protected} constructor.
* <br>
* Having the generated class be non-{@code final} allows the syntax
* <pre>
* {@code @Preferences(name = "Prefs$Generated", ...)}
* {@code public class Prefs extends Prefs$Generated {}}</pre>
* Either way the generated class will throw an exception on instantiation.
*/
boolean makeFile() default false;
}

@ -0,0 +1,336 @@
package eu.jonahbauer.android.preference.annotations.processor;
import eu.jonahbauer.android.preference.annotations.Preference;
import eu.jonahbauer.android.preference.annotations.Preferences;
import eu.jonahbauer.android.preference.annotations.PreferenceGroup;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;
import java.util.function.Function;
@SupportedAnnotationTypes({
"eu.jonahbauer.android.preference.annotations.Preferences"
})
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public final class PreferenceProcessor extends AbstractProcessor {
private static final String PACKAGE_DECLARATION = "package %s ;%n";
private static final String IMPORT_DECLARATION = "import android.content.SharedPreferences;%n" +
"import android.content.res.Resources;%n" +
"import java.util.Objects;%n" +
"import %s;%n";
private static final String CLASS_DECLARATION_START = "class %1$s {%n" +
" protected %1$s() {%n" +
" throw new IllegalStateException(\"This class is not supposed to be instantiated.\");%n" +
" }%n";
private static final String CLASS_DECLARATION_START_FINAL = "public final class %1$s {%n" +
" private %1$s() {%n" +
" throw new IllegalStateException(\"This class is not supposed to be instantiated.\");%n" +
" }%n";
private static final String CLASS_PREFERENCES_DECLARATION = " private static SharedPreferences sharedPreferences;%n";
private static final String GROUP_FIELD_DECLARATION = " private static %1$s group$%2$d;%n";
private static final String CLASS_INITIALIZER_START = " /**%n" +
" * Initialize this preference class to use the given {@link SharedPreferences}.%n" +
" * This function is supposed to be called from the applications {@code onCreate()} method.%n" +
" * @param pSharedPreferences the {@link SharedPreferences} to be used. Not {@code null}.%n" +
" * @param pResources the {@link Resources} from which the preference keys should be loaded. Not {@code null}.%n" +
" * @throws IllegalStateException if this preference class has already been initialized.%n" +
" */%n" +
" public static void init(SharedPreferences pSharedPreferences, Resources pResources) {%n" +
" if (sharedPreferences != null) {%n" +
" throw new IllegalStateException(\"Preferences have already been initialized.\");%n" +
" }%n" +
" Objects.requireNonNull(pSharedPreferences, \"SharedPreferences must not be null.\");%n" +
" Objects.requireNonNull(pResources, \"Resources must not be null.\");%n" +
" sharedPreferences = pSharedPreferences;%n";
private static final String CLASS_INITIALIZER_FIELD = " group$%2$d = new %1$s(pResources);%n";
private static final String CLASS_INITIALIZER_END = " }%n";
private static final String GROUP_ACCESSOR_DECLARATION = " public static %1$s %1$s() {%n" +
" if (sharedPreferences == null) {%n" +
" throw new IllegalStateException(\"Preferences have not yet been initialized.\");%n" +
" }%n" +
" return group$%2$d;%n" +
" }%n";
private static final String GROUP_CLASS_DECLARATION_START = " public static final class %s {%n";
private static final String PROPERTY_KEY = " private final String key$%d;%n";
private static final String GROUP_CLASS_CONSTRUCTOR_START = " private %s(Resources resources) {%n";
private static final String PROPERTY_KEY_INITIALIZER = " key$%d = resources.getString(R.string.%s);%n";
private static final String GROUP_CLASS_CONSTRUCTOR_END = " }%n";
private static final String PROPERTY_DOCUMENTATION = " /**%n" +
" * %s%n" +
" * (default: {@code %s})%n" +
" */%n";
private static final String PROPERTY_GETTER_START = " public %s %s() {%n";
private static final String PROPERTY_GETTER_BODY_BOOLEAN = " return sharedPreferences.getBoolean(key$%d, %s);%n";
private static final String PROPERTY_GETTER_BODY_INTEGER = " return sharedPreferences.getInt(key$%d, %s);%n";
private static final String PROPERTY_GETTER_BODY_BYTE = " return (byte) sharedPreferences.getInt(key$%d, %s);%n";
private static final String PROPERTY_GETTER_BODY_SHORT = " return (short) sharedPreferences.getInt(key$%d, %s);%n";
private static final String PROPERTY_GETTER_BODY_CHAR = " return (char) sharedPreferences.getInt(key$%d, %s);%n";
private static final String PROPERTY_GETTER_BODY_LONG = " return sharedPreferences.getLong(key$%d, %s);%n";
private static final String PROPERTY_GETTER_BODY_FLOAT = " return sharedPreferences.getFloat(key$%d, %s);%n";
private static final String PROPERTY_GETTER_BODY_DOUBLE = " return Double.longBitsToDouble(sharedPreferences.getLong(key$%d, %s));%n";
private static final String PROPERTY_GETTER_BODY_STRING = " return sharedPreferences.getString(key$%d, %s);%n";
private static final String PROPERTY_GETTER_END = " }%n";
private static final String PROPERTY_SETTER_START = " public void %2$s(%1$s value) {%n";
private static final String PROPERTY_SETTER_BODY_BOOLEAN = " sharedPreferences.edit().putBoolean(key$%d, value).apply();%n";
private static final String PROPERTY_SETTER_BODY_INTEGER = " sharedPreferences.edit().putInt(key$%d, value).apply();%n";
private static final String PROPERTY_SETTER_BODY_BYTE = " sharedPreferences.edit().putInt(key$%d, (int) value).apply();%n";
private static final String PROPERTY_SETTER_BODY_SHORT = " sharedPreferences.edit().putInt(key$%d, (int) value).apply();%n";
private static final String PROPERTY_SETTER_BODY_CHAR = " sharedPreferences.edit().putInt(key$%d, (int) value).apply();%n";
private static final String PROPERTY_SETTER_BODY_LONG = " sharedPreferences.edit().putLong(key$%d, value).apply();%n";
private static final String PROPERTY_SETTER_BODY_FLOAT = " sharedPreferences.edit().putFloat(key$%d, value).apply();%n";
private static final String PROPERTY_SETTER_BODY_DOUBLE = " sharedPreferences.edit().putLong(key$%d, Double.doubleToRawLongBits(value)).apply();%n";
private static final String PROPERTY_SETTER_BODY_STRING = " sharedPreferences.edit().putString(key$%d, value).apply();%n";
private static final String PROPERTY_SETTER_END = " }%n";
private static final String KEY_CLASS_FIELD_DECLARATION = " private final Keys keys = new Keys();%n";
private static final String KEY_CLASS_ACCESSOR = " public Keys keys() {%n" +
" return keys;%n" +
" }%n";
private static final String KEY_CLASS_DECLARATION_START = " public final class Keys {%n" +
" private Keys() {}%n";
private static final String KEY_CLASS_PROPERTY_ACCESSOR = " public String %s() {%n" +
" return key$%d;%n" +
" }%n";
private static final String KEY_CLASS_DECLARATION_END = " }%n";
private static final String GROUP_CLASS_DECLARATION_END = " }%n";
private static final String CLASS_DECLARATION_END = "}%n";
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
var clazzes = roundEnv.getElementsAnnotatedWith(Preferences.class);
try {
for (Element clazz : clazzes) {
var root = clazz.getAnnotation(Preferences.class);
var source = processingEnv.getFiler().createSourceFile(root.name());
try (var out = new PrintWriter(source.openWriter())) {
writeRootClass(out, root);
}
}
return true;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void writeRootClass(PrintWriter out, Preferences root) throws IOException {
var groups = root.value();
var fqcn = root.name();
if (!StringUtils.isFQCN(root.name())) {
throw new IllegalArgumentException("Illegal preference class name " + root.name() + ".");
}
var pckg = fqcn.lastIndexOf('.') != -1 ? fqcn.substring(0, fqcn.lastIndexOf('.')) : "";
var cn = fqcn.lastIndexOf('.') != -1 ? fqcn.substring(fqcn.lastIndexOf('.') + 1) : fqcn;
var r = mirror(root, Preferences::r);
if (!pckg.isEmpty()) out.printf(PACKAGE_DECLARATION, pckg);
out.println();
out.printf(IMPORT_DECLARATION, r);
out.println();
if (root.makeFile()) {
out.printf(CLASS_DECLARATION_START_FINAL, cn);
} else {
out.printf(CLASS_DECLARATION_START, cn);
}
out.printf(CLASS_PREFERENCES_DECLARATION);
for (int i = 0; i < groups.length; i++) {
out.printf(GROUP_FIELD_DECLARATION, groups[i].name(), i);
}
out.println();
out.printf(CLASS_INITIALIZER_START);
for (int i = 0; i < groups.length; i++) {
out.printf(CLASS_INITIALIZER_FIELD, groups[i].name(), i);
}
out.printf(CLASS_INITIALIZER_END);
out.println();
for (int i = 0; i < groups.length; i++) {
out.printf(GROUP_ACCESSOR_DECLARATION, groups[i].name(), i);
}
out.println();
for (PreferenceGroup group : groups) {
if (!StringUtils.isJavaIdentifier(group.name())) {
throw new IllegalArgumentException("Illegal preference group name " + group.name() + ".");
}
if (!group.prefix().isEmpty() && !StringUtils.isJavaIdentifier(group.prefix())) {
throw new IllegalArgumentException("Illegal preference group prefix " + group.prefix() + ".");
}
if (!group.suffix().isEmpty() && !group.suffix().matches("\\p{javaJavaIdentifierPart}*")) {
throw new IllegalArgumentException("Illegal preference group suffix " + group.suffix() + ".");
}
writeGroupClass(out, group);
}
out.printf(CLASS_DECLARATION_END);
}
private void writeGroupClass(PrintWriter out, PreferenceGroup group) {
var preferences = group.value();
out.printf(GROUP_CLASS_DECLARATION_START, group.name());
// Keys
out.printf(KEY_CLASS_FIELD_DECLARATION);
for (int j = 0; j < preferences.length; j++) {
out.printf(PROPERTY_KEY, j);
}
out.println();
// Constructor
out.printf(GROUP_CLASS_CONSTRUCTOR_START, group.name());
for (int i = 0; i < preferences.length; i++) {
if (!StringUtils.isJavaIdentifier(preferences[i].name())) {
throw new IllegalArgumentException("Illegal preference name " + preferences[i].name() + ".");
}
if (!StringUtils.isJavaIdentifier(group.prefix() + preferences[i].name() + group.suffix())) {
throw new IllegalArgumentException("Illegal preference name " + preferences[i].name() + ".");
}
out.printf(PROPERTY_KEY_INITIALIZER, i, group.prefix() + preferences[i].name() + group.suffix());
}
out.printf(GROUP_CLASS_CONSTRUCTOR_END);
out.println();
// Key class accessor
out.printf(KEY_CLASS_ACCESSOR);
out.println();
// Getters
for (int j = 0; j < preferences.length; j++) {
writeGetter(out, preferences[j], j);
}
out.println();
// Setters
for (int j = 0; j < preferences.length; j++) {
writeSetter(out, preferences[j], j);
}
out.println();
// Key class
out.printf(KEY_CLASS_DECLARATION_START);
for (int j = 0; j < preferences.length; j++) {
out.printf(KEY_CLASS_PROPERTY_ACCESSOR, StringUtils.getMethodName(preferences[j].name()), j);
}
out.printf(KEY_CLASS_DECLARATION_END);
out.println();
out.printf(GROUP_CLASS_DECLARATION_END);
out.println();
}
private static void writeGetter(PrintWriter out, Preference preference, int index) {
var type = mirror(preference, Preference::type);
if (type.getKind() == TypeKind.VOID) return;
writeDocumentation(out, preference, type);
out.printf(PROPERTY_GETTER_START, type, StringUtils.getMethodName(preference.name()));
switch (type.getKind()) {
case BOOLEAN:
out.printf(PROPERTY_GETTER_BODY_BOOLEAN, index, getDefaultValue(preference, type));
break;
case BYTE:
out.printf(PROPERTY_GETTER_BODY_BYTE, index, getDefaultValue(preference, type));
break;
case CHAR:
out.printf(PROPERTY_GETTER_BODY_CHAR, index, getDefaultValue(preference, type));
break;
case SHORT:
out.printf(PROPERTY_GETTER_BODY_SHORT, index, getDefaultValue(preference, type));
break;
case INT:
out.printf(PROPERTY_GETTER_BODY_INTEGER, index, getDefaultValue(preference, type));
break;
case LONG:
out.printf(PROPERTY_GETTER_BODY_LONG, index, getDefaultValue(preference, type));
break;
case FLOAT:
out.printf(PROPERTY_GETTER_BODY_FLOAT, index, getDefaultValue(preference, type));
break;
case DOUBLE:
out.printf(PROPERTY_GETTER_BODY_DOUBLE, index, getDefaultValue(preference, type));
break;
}
if (String.class.getName().equals(type.toString())) {
out.printf(PROPERTY_GETTER_BODY_STRING, index, getDefaultValue(preference, type));
}
out.printf(PROPERTY_GETTER_END);
}
private static void writeSetter(PrintWriter out, Preference preference, int index) {
var type = mirror(preference, Preference::type);
if (type.getKind() == TypeKind.VOID) return;
writeDocumentation(out, preference, type);
out.printf(PROPERTY_SETTER_START, type, StringUtils.getMethodName(preference.name()));
switch (type.getKind()) {
case BOOLEAN:
out.printf(PROPERTY_SETTER_BODY_BOOLEAN, index);
break;
case BYTE:
out.printf(PROPERTY_SETTER_BODY_BYTE, index);
break;
case SHORT:
out.printf(PROPERTY_SETTER_BODY_SHORT, index);
break;
case CHAR:
out.printf(PROPERTY_SETTER_BODY_CHAR, index);
break;
case INT:
out.printf(PROPERTY_SETTER_BODY_INTEGER, index);
break;
case LONG:
out.printf(PROPERTY_SETTER_BODY_LONG, index);
break;
case FLOAT:
out.printf(PROPERTY_SETTER_BODY_FLOAT, index);
break;
case DOUBLE:
out.printf(PROPERTY_SETTER_BODY_DOUBLE, index);
break;
}
if (String.class.getName().equals(type.toString())) {
out.printf(PROPERTY_SETTER_BODY_STRING, index);
}
out.printf(PROPERTY_SETTER_END);
}
private static void writeDocumentation(PrintWriter out, Preference preference, TypeMirror type) {
if (preference.description().isEmpty()) return;
out.printf(PROPERTY_DOCUMENTATION, preference.description(), getDefaultValue(preference, type));
}
private static String getDefaultValue(Preference preference, TypeMirror type) {
if (!Preference.NO_DEFAULT_VALUE.equals(preference.defaultValue())) {
if (String.class.getName().equals(type.toString())) {
return "\"" + StringUtils.escape(preference.defaultValue()) + "\"";
} else {
return preference.defaultValue();
}
} else switch (type.getKind()) {
case BOOLEAN: return "false";
case BYTE: case CHAR: case SHORT: case INT: case LONG: case FLOAT: case DOUBLE: return "0";
case ARRAY: case NULL: case DECLARED: return "null";
default: throw new RuntimeException();
}
}
private static <S> TypeMirror mirror(S object, Function<S, ? extends Class<?>> function) {
try {
function.apply(object);
throw new RuntimeException();
} catch (MirroredTypeException e) {
return e.getTypeMirror();
}
}
}

@ -0,0 +1,54 @@
package eu.jonahbauer.android.preference.annotations.processor;
import java.util.Locale;
import java.util.regex.Pattern;
public final class StringUtils {
private static final Pattern IDENTIFIER = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");
private static final Pattern FQCN = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*");
/**
* Checks whether the given string is an identifier as per JLS 3.8, i.e. it starts with a
* {@linkplain Character#isJavaIdentifierStart(char) Java Letter} and all the remaining characters are
* {@linkplain Character#isJavaIdentifierPart(char) Java Letters or Digits}.
* @param string the string to be checked
*/
static boolean isJavaIdentifier(String string) {
return IDENTIFIER.matcher(string).matches();
}
/**
* Checks whether the given string is a fully qualified class name as per JLS 6.7, i.e. a dot-seperated list of
* {@linkplain #isJavaIdentifier(String) identifiers}.
* @param string the string to be checked
*/
static boolean isFQCN(String string) {
return FQCN.matcher(string).matches();
}
static String getMethodName(String preferenceName) {
int index;
while ((index = preferenceName.indexOf('_')) != -1) {
if (index == preferenceName.length() - 1) preferenceName = preferenceName.substring(0, index);
preferenceName = preferenceName.substring(0, index)
+ preferenceName.substring(index + 1, index + 2).toUpperCase(Locale.ROOT)
+ preferenceName.substring(index + 2);
}
return preferenceName;
}
/**
* Escapes the given string for use in a String literal in Java source code. Note that quotes are not added
* automatically.
*/
static String escape(String string) {
return string.replace("\\", "\\\\")
.replace("\t", "\\t")
.replace("\b", "\\b")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\f", "\\f")
.replace("'", "\\'")
.replace("\"", "\\\"");
}
}

@ -1,2 +0,0 @@
module eu.jonahbauer.android.preference.annotations {
}

@ -0,0 +1 @@
eu.jonahbauer.android.preference.annotations.processor.PreferenceProcessor
Loading…
Cancel
Save