diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..26bd9f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +###OSX### + +.DS_Store +.AppleDouble +.LSOverride + +# Icon must ends with two \r. +Icon + + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + + +###Linux### + +*~ + +# KDE directory preferences +.directory + + +###Android### + +# Built application files +*.apk +*.ap_ + +# Files for ART and Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ + +# Gradle files +.gradle/ +.gradletasknamecache +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Lint +lint-report.html +lint-report_files/ +lint_result.txt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + + +###IntelliJ### + +*.iml +*.ipr +*.iws +.idea/ +captures/ + + +###Eclipse### + +*.pydevproject +.metadata +tmp/ +*.tmp +*.bak +*.swp +*~.nib +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..50d8cc6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: android + +android: + components: + - build-tools-22.0.1 + - android-22 + - extra-android-m2repository + +jdk: oraclejdk8 + +notifications: + email: false + +sudo: false + +cache: + directories: + - $HOME/.gradle + +script: ./gradlew build test \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3429618 --- /dev/null +++ b/LICENSE @@ -0,0 +1,139 @@ +Copyright (c) 2015, Flipboard +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* Neither the name of Flipboard nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------- + +NOTICE for Android Gradle Plugin and Android +Copyright (c) 2015, Android Open Source Project +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------- + +NOTICE for Javapoet +Copyright 2015 Square, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------- + +NOTICE for Commons-lang3 +Apache Commons Lang +Copyright 2001-2015 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This product includes software from the Spring Framework, +under the Apache License 2.0 (see: StringUtils.containsWhitespace()) + +-------------------------------------------------------------------------- + +NOTICE for Guava +Copyright 2015 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------- + +NOTICE for RxGroovy +Copyright 2012 Netflix, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------- + +NOTICE for Truth +Copyright 2015 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------- + +NOTICE for JavaParser +Copyright 2015 JavaParser + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec301b3 --- /dev/null +++ b/README.md @@ -0,0 +1,205 @@ +# PSync + +PSync is a gradle plugin for android projects to generate Java representations of xml preferences. + +Some applications have a lot of preferences, each their own keys, default values, and more. These tend to be + stored in xml files (under `res/xml`), and don't have any programmatic linking of their values. The result? + You have to *manually* keep these values in sync with your Java code. Yikes! + +We got tired of dealing with this at Flipboard. Our preference class ended up with 200+ lines of boilerplate +at the top that we manually had to keep in sync, and it was becoming a nuisance. PSync was developed +to resolve this, and we hope it helps you too! + +## Setup + +Apply the PSync plugin to your module *below* your android plugin application + +```groovy +buildscript { + repositories { + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath "gradle.plugin.com.flipboard:psync:1.1.5" + } +} + +apply plugin: 'com.android.application' // Can be library too +apply plugin: 'com.flipboard.psync' +``` + +The PSync plugin will create a generating task for each of your variants, and will generate a file +at compilation that will be included in your classpath. This process is purposefully very similar to +how the `R.java` file works. The default name for this class is `P.java`, but you can configure it to +be another name if you wish. + +Speaking of configuration, here's how you can configure PSync to work for you. + +```groovy +psync { + className = "MyClassName" + includesPattern = "**/xml/.xml" + packageName = "com.example.myapp" + generateRx = true +} +``` + +Let's take a look at each: + +**className** is the name of the class. The default is just `P`. + +**includesPattern** is an ant-style includes pattern for preference xml files in your resources. This +is useful if you have a lot of other xml files and want to save the plugin a little work by filtering +out non-pref ones. In Flipboard, all our preference files are prefixed with `prefs_`, so our includes +pattern would be `**/xml/prefs_*.xml`. The default is to parse all xml files in `xml` resource directories. + +**packageName** is what you want to use for the package name of the generated resource classes. The +default behavior of the plugin is to retrieve this from your `variant`'s `applicationId` value. **NOTE:** +Library projects **MUST** specify this, since they don't have `applicationId` values. + +**generateRx** is a flag indicating whether or not you want code generated for usage with [Rx-Preferences](https://github.com/f2prateek/rx-preferences), +which is a great library that adds reactive bindings around SharedPreferences + +## Usage + +Using the generated file is easy, and should feel very familiar to how you would use `R.java`. + +Each preference is represented by a static inner class, with a `key` field, a `defaultResId` field if +it's a resource, and some or all of following functions: +* `defaultValue()` for retrieving the default value +* `get()` for retrieving the current stored value +* `put()` for storing a new value + * NOTE: This returns an `Editor` instance, where you much `apply` or `commit` the change(s) +* `rx()` for retrieving an appropriate `Rx-Preferences` instance of `Preference`, if you enabled `generateRx` above. + +These functions are generated when appropriate. If no default value or resource reference is specified, +the plugin will not try to guess the type and generate code for it. + +First thing's first: Initialize your P.java file in your Application's `onCreate()` method. + +```java +public class MyApp extends Application { + + @Override + public void onCreate() { + + // Required + P.init(this); + + // Optional + // By default, it will initialize its internal SharedPreferences instance to the system default + // You can change its used instance at any time + P.setSharedPreferences(myOtherInstance); + } + +} +``` + +Let's take a look at an example. The following xml preference: + +```xml + +``` + +Becomes this Java code: + +```java +public final class P { + public static final class showImages { + public static final String key = "show_images"; + + public static final boolean defaultValue() { + return true; + } + + public static final boolean get() { + return PREFERENCES.getBoolean(key, defaultValue()); + } + + public static final SharedPreferences.Editor put(final boolean val) { + return PREFERENCES.edit().putBoolean(key, val); + } + + public static final Preference rx() { + return RX_SHARED_PREFERENCES.getBoolean(key); + } + } +} +``` + +You can now reference this in code like so: + +```java +String theKey = P.showImages.key; +boolean theDefault = P.showImages.defaultValue(); +boolean current = P.showImages.get(); +P.showImages.put(false).apply(); + +// If you use Rx-Preferences +P.showImages.rx().asObservable().omgDoRxStuff! +``` + +Nice and simple right? Note that the entry block's name will be a camelCaseLower conversion of the key. + +Let's look at a resource example now. The following preference: + +```xml + +``` + +Becomes the following Java code: + +```java +public final class P { + public static final class serverUrl { + public static final String key = "server_url"; + public static final int defaultResId = R.string.server_url; + + public static final String defaultValue() { + return RESOURCES.getString(defaultResId); + } + + public static final String get() { + return PREFERENCES.getString(key, defaultValue()); + } + + public static final SharedPreferences.Editor put(final String val) { + return PREFERENCES.edit().putString(key, val); + } + + public static final Preference rx() { + return RX_SHARED_PREFERENCES.getString(key); + } + } +} +``` + +Notice that resources are handled a little differently. This is intentional, and for convenience. Using +this code now looks like this: + +```java +String theKey = P.serverUrl.key; +int theResId = P.serverUrl.defaultResId; +String theDefault = P.serverUrl.defaultValue(); +String currentValue = P.serverUrl.get(); +P.serverUrl.put("https://example.com").apply(); + +// If you use Rx-Preferences +P.serverUrl.rx().asObservable().omgDoRxStuff! +``` + +Easy peasy. Enjoy! + +## Contributing +We welcome pull requests for bug fixes, new features, and improvements to PSync. Contributors +to PSync repository must accept Flipboard's Apache-style +[Individual Contributor License Agreement (CLA)](https://docs.google.com/forms/d/1gh9y6_i8xFn6pA15PqFeye19VqasuI9-bGp_e0owy74/viewform) +before any changes can be merged. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..70056f0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,62 @@ +buildscript { + repositories { + jcenter() + maven { + url "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + classpath "com.gradle.publish:plugin-publish-plugin:0.9.1" + classpath "gradle.plugin.com.flipboard:psync:1.1.5" + } +} + +allprojects { + repositories { + jcenter() + } +} + +ext { + VERSION = version() +} + +task bumpMajor << { + ant.propertyfile(file: 'version.properties') { + entry(key: 'major', type: 'int', operation: '+', value: 1) + entry(key: 'minor', type: 'int', operation: '=', value: 0) + entry(key: 'patch', type: 'int', operation: '=', value: 0) + } +} + +task bumpMinor << { + ant.propertyfile(file: 'version.properties') { + entry(key: 'minor', type: 'int', operation: '+', value: 1) + entry(key: 'patch', type: 'int', operation: '=', value: 0) + } +} + +task bumpPatch << { + ant.propertyfile(file: 'version.properties') { + entry(key: 'patch', type: 'int', operation: '+', value: 1) + } +} + +task genReadMe << { + def template = file('README.md.template').text + def result = template.replaceAll("%%version%%", version()) + file("README.md").withWriter{ it << result } +} + +task version << { + println version() +} + +def String version() { + def versionPropsFile = file('version.properties') + def Properties versionProps = new Properties() + versionProps.load(new FileInputStream(versionPropsFile)) + + return versionProps['major'] + "." + versionProps['minor'] + "." + versionProps['patch'] +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1d3591c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b5166da Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afe7d2f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Aug 05 16:39:54 PDT 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.6-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/psync/build.gradle b/psync/build.gradle new file mode 100644 index 0000000..62faca8 --- /dev/null +++ b/psync/build.gradle @@ -0,0 +1,47 @@ +/* + * Copyright 2015 Flipboard Inc + */ +apply plugin: 'com.gradle.plugin-publish' +apply plugin: 'groovy' +apply plugin: 'maven' + +repositories { + jcenter() +} + +dependencies { + compile gradleApi() + compile localGroovy() + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.tools.build:gradle:1.3.1' + compile 'com.google.android:android:4.1.1.4' + compile 'com.squareup:javapoet:1.2.0' + compile 'org.apache.commons:commons-lang3:3.4' + compile 'com.google.guava:guava:18.0' + compile('io.reactivex:rxgroovy:1.0.0') { + exclude module: "groovy-all" + } + testCompile 'junit:junit:4.12' + testCompile 'com.google.truth:truth:0.27' + testCompile 'com.github.javaparser:javaparser-core:2.1.0' +} + +// Plugin publishing +version = project.property('VERSION') +group = 'com.flipboard' + +if (project.hasProperty('gradle.publish.key')) { + pluginBundle { + website = 'https://github.com/Flipboard/psync' + vcsUrl = 'https://github.com/Flipboard/psync' + description = 'Gradle plugin that generates class representations of xml preferences' + tags = ['gradle', 'plugin', 'android'] + plugins { + psyncPlugin { + id = 'com.flipboard.psync' + displayName = 'PSync Plugin' + version = project.property('VERSION') + } + } + } +} diff --git a/psync/src/main/groovy/com/flipboard/psync/PClassGenerator.java b/psync/src/main/groovy/com/flipboard/psync/PClassGenerator.java new file mode 100644 index 0000000..1093e8d --- /dev/null +++ b/psync/src/main/groovy/com/flipboard/psync/PClassGenerator.java @@ -0,0 +1,265 @@ +package com.flipboard.psync; + +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.preference.PreferenceManager; + +import com.google.common.base.CaseFormat; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; + +import org.apache.commons.lang3.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.regex.Pattern; + +import javax.lang.model.element.Modifier; + +/** + * Separate class in Java for generating the code because Groovy can't talk to Java vararg methods + */ +public final class PClassGenerator { + + private static final ClassName CN_RX_PREFERENCES = ClassName.get("com.f2prateek.rx.preferences", "RxSharedPreferences"); + private static final ClassName CN_RX_PREFERENCE = ClassName.get("com.f2prateek.rx.preferences", "Preference"); + private static final Modifier[] MODIFIERS = {Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL}; + private static final Pattern COULD_BE_CAMEL = Pattern.compile("[a-zA-Z]+[a-zA-Z0-9]*"); + private static final Pattern ALL_CAPS = Pattern.compile("[A-Z0-9]*"); + + /** + * Groovy can't talk to Java vararg methods, such as JavaPoet's many vararg methods. Utility + * class is here so we can use JavaPoet nicely. + * + * @param inputKeys List of the preference keys to generate for + * @param packageName Package name to create the P class in + * @param outputDir Output directory to create the P.java file in + * @param className Name to use for the generated class + * @param generateRx Boolean indicating whether or not to generate Rx-Preferences support code + * @throws IOException because Java + */ + public static void generate(List inputKeys, String packageName, File outputDir, String className, boolean generateRx) throws IOException { + TypeSpec.Builder pClass = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + setUpContextAndPreferences(pClass, generateRx); + + pClass.addMethod(MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addStatement("throw new $T($S)", AssertionError.class, "No instances.") + .build() + ); + + for (PrefEntry entry : inputKeys) { + pClass.addType(generatePrefBlock(entry, packageName, generateRx)); + } + + JavaFile javaFile = JavaFile.builder(packageName, pClass.build()).build(); + javaFile.writeTo(outputDir); + } + + private static void setUpContextAndPreferences(TypeSpec.Builder pClass, boolean generateRx) { + pClass.addField(FieldSpec.builder(Resources.class, "RESOURCES", Modifier.PRIVATE, Modifier.STATIC) + .initializer("null") + .build() + ); + + pClass.addField(FieldSpec.builder(SharedPreferences.class, "PREFERENCES", Modifier.PRIVATE, Modifier.STATIC) + .initializer("null") + .build() + ); + + if (generateRx) { + pClass.addField(FieldSpec.builder(CN_RX_PREFERENCES, "RX_PREFERENCES", Modifier.PRIVATE, Modifier.STATIC) + .initializer("null") + .build() + ); + } + + pClass.addMethod(MethodSpec.methodBuilder("init") + .addModifiers(MODIFIERS) + .addJavadoc("Initializer that takes a {@link Context} for resource resolution. This should be an Application context instance, and will retrieve default shared preferences.\n") + .addParameter(ParameterSpec.builder(Context.class, "applicationContext", Modifier.FINAL).build()) + .beginControlFlow("if (applicationContext == null)") + .addStatement("throw new $T($S)", IllegalStateException.class, "applicationContext cannot be null!") + .endControlFlow() + .beginControlFlow("if (!(applicationContext instanceof $T))", Application.class) + .addStatement("throw new $T($S)", IllegalArgumentException.class, "You may only use an Application instance as context!") + .endControlFlow() + .addStatement("RESOURCES = applicationContext.getResources()") + .addStatement("// Sensible default") + .addStatement("setSharedPreferences($T.getDefaultSharedPreferences(applicationContext))", PreferenceManager.class) + .build() + ); + + MethodSpec.Builder setSharedPreferencesBuilder = MethodSpec.methodBuilder("setSharedPreferences") + .addModifiers(MODIFIERS) + .addParameter(ParameterSpec.builder(SharedPreferences.class, "sharedPreferences", Modifier.FINAL).build()) + .beginControlFlow("if (sharedPreferences == null)") + .addStatement("throw new $T($S)", IllegalStateException.class, "sharedPreferences cannot be null!") + .endControlFlow() + .addStatement("PREFERENCES = sharedPreferences"); + + if (generateRx) { + setSharedPreferencesBuilder.addStatement("RX_PREFERENCES = $T.create(PREFERENCES)", CN_RX_PREFERENCES); + } + + pClass.addMethod(setSharedPreferencesBuilder.build()); + } + + private static TypeSpec generatePrefBlock(PrefEntry entry, String packageName, boolean generateRx) { + TypeSpec.Builder entryClass = TypeSpec.classBuilder(camelCaseKey(entry.key)).addModifiers(MODIFIERS); + entryClass.addField(FieldSpec.builder(String.class, "key", MODIFIERS).initializer("$S", entry.key).build()); + + if (entry.defaultType != null) { + if (entry.isResource) { + entryClass.addField(FieldSpec.builder(int.class, "defaultResId", MODIFIERS).initializer("$T.$N.$N", ClassName.get(packageName, "R"), entry.resType, entry.defaultValue).build()); + entryClass.addMethod(generateResolveDefaultResMethod(entry)); + } else { + boolean isString = entry.defaultType == String.class; + entryClass.addMethod(MethodSpec.methodBuilder("defaultValue") + .addModifiers(MODIFIERS) + .returns(entry.defaultType) + .addStatement("return " + (isString ? "$S" : "$N"), isString ? entry.defaultValue : entry.defaultValue.toString()) + .build()); + } + } + + // Add getter + if (entry.valueType != null || entry.defaultType != null) { + Class prefType = entry.valueType != null ? entry.valueType : entry.defaultType; + entryClass.addMethod(MethodSpec.methodBuilder("get") + .addModifiers(MODIFIERS) + .returns(prefType) + .addStatement("return PREFERENCES.$N", resolvePreferenceStmt(entry, true)) + .build() + ); + + entryClass.addMethod(MethodSpec.methodBuilder("put") + .addModifiers(MODIFIERS) + .returns(SharedPreferences.Editor.class) + .addParameter(ParameterSpec.builder(prefType, "val", Modifier.FINAL).build()) + .addStatement("return PREFERENCES.edit().$N", resolvePreferenceStmt(entry, false)) + .build() + ); + + Class referenceType = resolveReferenceType(prefType); + if (generateRx && referenceType != null) { + entryClass.addMethod(MethodSpec.methodBuilder("rx") + .addModifiers(MODIFIERS) + .returns(ParameterizedTypeName.get(CN_RX_PREFERENCE, TypeName.get(referenceType))) + .addStatement("return RX_PREFERENCES.get$N(key)", referenceType.getSimpleName()) + .build() + ); + } + } + + if (entry.hasListAttributes) { + if (entry.entriesGetterStmt != null) { + entryClass.addMethod(MethodSpec.methodBuilder("entries") + .addModifiers(MODIFIERS) + .returns(CharSequence[].class) + .addStatement("return RESOURCES." + entry.entriesGetterStmt) + .build() + ); + } + if (entry.entryValuesGetterStmt != null) { + entryClass.addMethod(MethodSpec.methodBuilder("entryValues") + .addModifiers(MODIFIERS) + .returns(CharSequence[].class) + .addStatement("return RESOURCES." + entry.entryValuesGetterStmt) + .build() + ); + } + } + + return entryClass.build(); + } + + static String camelCaseKey(String input) { + + // Default to lower_underscore, as this is the platform convention + CaseFormat format = CaseFormat.LOWER_UNDERSCORE; + + boolean couldBeCamel = COULD_BE_CAMEL.matcher(input).matches(); + if (!couldBeCamel) { + if (input.contains("-")) { + format = CaseFormat.LOWER_HYPHEN; + } else if (input.contains("_")) { + boolean isAllCaps = ALL_CAPS.matcher(input).matches(); + format = isAllCaps ? CaseFormat.UPPER_UNDERSCORE : CaseFormat.LOWER_UNDERSCORE; + } + } else { + format = Character.isUpperCase(input.charAt(0)) ? CaseFormat.UPPER_CAMEL : CaseFormat.LOWER_CAMEL; + } + + return format == CaseFormat.LOWER_CAMEL ? input : format.to(CaseFormat.LOWER_CAMEL, input); + } + + private static MethodSpec generateResolveDefaultResMethod(PrefEntry entry) { + return MethodSpec.methodBuilder("defaultValue") + .addModifiers(MODIFIERS) + .returns(entry.valueType) + .addCode(CodeBlock.builder().addStatement("return RESOURCES.$N", entry.resourceDefaultValueGetterStmt).build()) + .build(); + } + + private static Class resolveReferenceType(Class clazz) { + if (!clazz.isPrimitive()) { + return clazz; + } + switch (clazz.getSimpleName()) { + case "Boolean": + case "boolean": + return Boolean.class; + case "Integer": + case "int": + return Integer.class; + default: + // Currently unsupported + return null; + } + } + + private static String resolvePreferenceStmt(PrefEntry entry, boolean isGetter) { + String defaultValue = "defaultValue()"; + String simpleName = StringUtils.capitalize(entry.valueType.getSimpleName()); + if (entry.defaultType == null) { + // No defaultValue() method will be available + switch (simpleName) { + case "Boolean": + defaultValue = "false"; + break; + case "Int": + defaultValue = "-1"; + break; + case "String": + defaultValue = "null"; + break; + default: + defaultValue = "null"; + break; + } + } + + if (isGetter) { + return "get" + simpleName + "(key, " + defaultValue + ")"; + } else { + return "put" + simpleName + "(key, val)"; + } + } + + private PClassGenerator() { + throw new AssertionError("No instances."); + } + +} diff --git a/psync/src/main/groovy/com/flipboard/psync/PSyncPlugin.groovy b/psync/src/main/groovy/com/flipboard/psync/PSyncPlugin.groovy new file mode 100644 index 0000000..9037658 --- /dev/null +++ b/psync/src/main/groovy/com/flipboard/psync/PSyncPlugin.groovy @@ -0,0 +1,65 @@ +/* + * Copyright 2015 Flipboard Inc + */ + +package com.flipboard.psync + +import com.android.build.gradle.api.BaseVariant +import org.gradle.api.Plugin +import org.gradle.api.Project + +class PSyncPlugin implements Plugin { + + void apply(Project project) { + project.extensions.create('psync', PSyncPluginExtension) + + project.afterEvaluate { + + // Make sure there's an android configuration + if (!project.android) { + throw new IllegalStateException('Must apply \'com.android.application\' or \'com.android.library\' first!') + } + + // Determine our variants and what type of project we're in + //noinspection GroovyUnusedAssignment + def variants = null + def isApp = false; + if (project.android.hasProperty('applicationVariants')) { + variants = project.android.applicationVariants + isApp = true + } else if (project.android.hasProperty('libraryVariants')) { + variants = project.android.libraryVariants + } else { + throw new IllegalStateException('Android project must have applicationVariants or libraryVariants!') + } + + // Determine the package name + String resolvedPackageName = project.psync.packageName + if (!resolvedPackageName) { + if (isApp) { + resolvedPackageName = project.android.defaultConfig.applicationId + } else { + throw new IllegalStateException('You must specify a package name for library projects!') + } + + } + + String includesPattern = project.psync.includesPattern + + // Register our task with the variant + variants.all { BaseVariant variant -> + + PSyncTask psyncTask = (PSyncTask) project.task(type: PSyncTask, "generatePrefKeysFor${variant.name.capitalize()}") { + source = variant.getSourceSets().collect { it.getResDirectories() } + include includesPattern + outputDir = project.file("$project.buildDir/generated/source/psync/$variant.flavorName/$variant.buildType.name/") + packageName = resolvedPackageName + className = project.psync.className + generateRx = project.psync.generateRx + } + + variant.registerJavaGeneratingTask(psyncTask, (File) psyncTask.outputDir) + } + } + } +} diff --git a/psync/src/main/groovy/com/flipboard/psync/PSyncPluginExtension.groovy b/psync/src/main/groovy/com/flipboard/psync/PSyncPluginExtension.groovy new file mode 100644 index 0000000..e97af01 --- /dev/null +++ b/psync/src/main/groovy/com/flipboard/psync/PSyncPluginExtension.groovy @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Flipboard Inc + */ + +package com.flipboard.psync + +/** + * Configuration values for the PSync plugin. + */ +class PSyncPluginExtension { + + /** + * Ant-style includes pattern for identifying what files you want to include in the XML parsing. + * + * Note that this is relative to the res directory ('src/main/res') + * + * Default is to parse all files in the directory. + */ + String includesPattern = "**/xml/*.xml" + + /** + * Package name you want the generated class to be in. Default is to use the applicationID. + * + * REQUIRED for _library_ projects. + */ + String packageName = null + + /** + * Name you want for the generated class. Default is "P" + */ + String className = "P" + + /** + * Enable this to generate rx() methods for preference blocks, which uses + * f2prateek's Rx-Preferences library. Note that this will fail compilation if you don't include + * it as a dependency. + * + * See https://github.com/f2prateek/rx-preferences + * + * Default is false + */ + boolean generateRx = false; + +} diff --git a/psync/src/main/groovy/com/flipboard/psync/PSyncTask.groovy b/psync/src/main/groovy/com/flipboard/psync/PSyncTask.groovy new file mode 100644 index 0000000..0c8c8d9 --- /dev/null +++ b/psync/src/main/groovy/com/flipboard/psync/PSyncTask.groovy @@ -0,0 +1,154 @@ +/* + * Copyright 2015 Flipboard Inc + */ + +package com.flipboard.psync +import com.google.common.collect.ImmutableList +import groovy.xml.QName +import org.apache.commons.lang3.StringUtils +import org.apache.commons.lang3.math.NumberUtils +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.SourceTask +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.incremental.IncrementalTaskInputs +import rx.Observable + +/** + * Task that generates P.java source files + */ +class PSyncTask extends SourceTask { + + static final ImmutableList BOOL_TYPES = ImmutableList.of("true", "false") + + /** + * The output directory. + */ + @OutputDirectory + File outputDir + + @Input + String packageName + + @Input + String className + + @Input + boolean generateRx + + @TaskAction + def generate(IncrementalTaskInputs inputs) { + + // If the whole thing isn't incremental, delete the build folder (if it exists) + // TODO If they change the className, we should probably delete the old one for good measure if it exists + if (!inputs.isIncremental() && outputDir.exists()) { + logger.debug("PSync generation is not incremental; deleting build folder and starting fresh!") + outputDir.deleteDir() + } + + if (!outputDir.exists()) { + outputDir.mkdirs() + } + + List entries = getPrefEntriesFromFiles(getSource()).toBlocking().first() + + PClassGenerator.generate(entries, packageName, outputDir, className, generateRx) + } + + /** + * Retrieves all the keys in the files in a given xml directory + * + * @param xmlDir Directory to search + * @param fileRegex Regex for matching the files you want + * @return Observable of all the distinct keys in this directory. + */ + static Observable> getPrefEntriesFromFiles(Iterable sources) { + Observable.from(sources) // Fetch the keys from each file + .map {file -> new XmlParser().parse(file)} // Parse the file + .flatMap {rootNode -> Observable.from(rootNode.depthFirst())} // Extract all the nodes + .map {Node node -> generatePrefEntry(node.attributes())} // Generate PrefEntry objects from the attributes + .filter {PrefEntry entry -> !entry.isBlank()} // Filter out ones we can't use + .distinct() // Only want unique + .toSortedList() // Output the sorted list + } + + /** + * Generates a {@link PrefEntry} from the given attributes on a Node + * + * @param attributes attributes on the node to parse + * @return a generated PrefEntry, or {@link PrefEntry#BLANK} if we can't do anything with it + */ + static PrefEntry generatePrefEntry(Map attributes) { + PrefEntry entry + String key = null + String defaultValue = null + + // These are present for list-type preferences + String entries = null + String entryValues = null + + attributes.entrySet().each { Map.Entry attribute -> + String name = attribute.key.localPart + switch (name) { + case "key": + key = attribute.value + break + case "defaultValue": + defaultValue = attribute.value + break + case "entries": + entries = attribute.value + break + case "entryValues": + entryValues = attribute.value + break + } + } + + if (StringUtils.isEmpty(key)) { + return PrefEntry.BLANK + } + + boolean hasListAttributes = entries || entryValues + + if (defaultValue == null || defaultValue.length() == 0) { + entry = PrefEntry.create(key, null) + } else if (BOOL_TYPES.contains(defaultValue)) { + entry = PrefEntry.create(key, Boolean.valueOf(defaultValue)) + } else if (NumberUtils.isNumber(defaultValue)) { + entry = PrefEntry.create(key, Integer.valueOf(defaultValue)) + } else if (defaultValue.startsWith('@')) { + entry = generateResourcePrefEntry(key, defaultValue) + if (hasListAttributes && entry.resType == "string") { + // Only string resource entries can be list preferences + entry.markAsListPreference(entries, entryValues) + } + } else { + entry = PrefEntry.create(key, defaultValue) + if (hasListAttributes) { + entry.markAsListPreference(entries, entryValues) + } + } + + return entry + } + + /** + * Resource PrefEntries are special, because we need to retrieve their resource ID. + * + * @param key Preference key + * @param defaultValue String representation of the default value (e.g. "@string/hello") + * @return PrefEntry object representing this, or {@link PrefEntry#BLANK} if we couldn't resolve its resource ID + */ + static PrefEntry generateResourcePrefEntry(String key, String defaultValue) { + String[] split = defaultValue.split('/') + + if (split == null || split.length < 2) { + return PrefEntry.BLANK; + } + + String resType = split[0].substring(1) + String resId = split[1] + return PrefEntry.create(key, resId, resType) + } +} diff --git a/psync/src/main/groovy/com/flipboard/psync/PrefEntry.java b/psync/src/main/groovy/com/flipboard/psync/PrefEntry.java new file mode 100644 index 0000000..bd54ccc --- /dev/null +++ b/psync/src/main/groovy/com/flipboard/psync/PrefEntry.java @@ -0,0 +1,176 @@ +package com.flipboard.psync; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +/** + * This represents a preference entry + *

+ * T represents the type of value this preference is backed by, such as a boolean + */ +public final class PrefEntry implements Comparable { + + public static final PrefEntry BLANK = new PrefEntry<>("", null, null); + + public static PrefEntry create(@NotNull String key, T defaultValue) { + return create(key, defaultValue, null); + } + + public static PrefEntry create(@NotNull String key, T defaultValue, String resType) { + return new PrefEntry<>(key, defaultValue, resType); + } + + public String key; + public T defaultValue; + public Class defaultType; + public Class valueType; + + // For list types + public boolean hasListAttributes = false; + public String entriesGetterStmt = null; + public String entryValuesGetterStmt = null; + + // Resource specific info + public String resType = null; + public boolean isResource = false; + public String resourceDefaultValueGetterStmt = null; + + private PrefEntry(String key, T defaultValue, String resType) { + this.key = key; + this.defaultValue = defaultValue; + + if (resType != null) { + this.resType = resType; + this.isResource = true; + } + + if (defaultValue == null) { + this.valueType = this.defaultType = null; + } else if (isResource) { + this.defaultType = String.class; + resolveResourceInfo(); + } else if (defaultValue instanceof Boolean) { + this.valueType = this.defaultType = boolean.class; + } else if (defaultValue instanceof Integer) { + this.valueType = this.defaultType = int.class; + } else if (defaultValue instanceof String) { + this.valueType = this.defaultType = String.class; + } else { + throw new UnsupportedOperationException("Unsupported type: " + defaultValue.getClass().getSimpleName()); + } + } + + /** + * Marks this entry as a list preference if the passed parameters are value @array resource refs + */ + void markAsListPreference(String entries, String entryValues) { + String entriesName = getArrayResourceName(entries); + if (entriesName != null) { + this.entriesGetterStmt = "getTextArray(R.array." + entriesName + ")"; + this.hasListAttributes = true; + } + String entryValuesName = getArrayResourceName(entryValues); + if (entryValuesName != null) { + this.entryValuesGetterStmt = "getTextArray(R.array." + entryValuesName + ")"; + this.hasListAttributes = true; + } + } + + private static String getArrayResourceName(String ref) { + if (ref == null || !ref.startsWith("@array/") || ref.length() < 7) { + return null; + } + + return ref.substring(7); + } + + @Override + public int hashCode() { + return key.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } else if (o == null || getClass() != o.getClass()) { + return false; + } + + return hashCode() == o.hashCode(); + } + + @Override + public int compareTo(@NotNull PrefEntry o) { + return this.key.compareTo(o.key); + } + + @Override + public String toString() { + return "PrefEntry{key=" + + this.key + + ", defaultValue=" + + this.defaultValue + + ", defaultType=" + + this.defaultType + + ", valueType=" + + this.valueType + + ", resType=" + + this.resType + + ", isResource=" + + this.isResource + + ", resourceDefaultValueGetterStmt=" + + this.resourceDefaultValueGetterStmt + + ", hasListAttributes=" + + hasListAttributes + + ", entriesGetterStmt=" + + entriesGetterStmt + + ", entryValuesGetterStmt=" + + entryValuesGetterStmt + + "}"; + } + + public boolean isBlank() { + return StringUtils.isEmpty(key); + } + + // TODO + // Float + // Long + // String set + // String[] overload for array + // int[] overload for array + private void resolveResourceInfo() { + final String type = resType; + String statement; + + switch (type) { + case "bool": + statement = "getBoolean(%s)"; + valueType = boolean.class; + break; + case "color": + statement = "getColor(%s)"; + valueType = int.class; + break; + case "integer": + statement = "getInteger(%s)"; + valueType = int.class; + break; + case "string": + statement = "getString(%s)"; + valueType = String.class; + break; + case "array": + statement = "getTextArray(%s)"; + valueType = CharSequence[].class; + break; + default: + // We can't handle this type yet, force it to blank out + this.key = ""; + return; + } + + resourceDefaultValueGetterStmt = String.format(statement, "defaultResId"); + } +} diff --git a/psync/src/main/resources/META-INF/gradle-plugins/com.flipboard.psync.properties b/psync/src/main/resources/META-INF/gradle-plugins/com.flipboard.psync.properties new file mode 100644 index 0000000..f1a446b --- /dev/null +++ b/psync/src/main/resources/META-INF/gradle-plugins/com.flipboard.psync.properties @@ -0,0 +1 @@ +implementation-class=com.flipboard.psync.PSyncPlugin \ No newline at end of file diff --git a/psync/src/test/fixtures/android_app/src/main/AndroidManifest.xml b/psync/src/test/fixtures/android_app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f8a87f3 --- /dev/null +++ b/psync/src/test/fixtures/android_app/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/psync/src/test/fixtures/android_app/src/main/res/xml/prefs.xml b/psync/src/test/fixtures/android_app/src/main/res/xml/prefs.xml new file mode 100644 index 0000000..3a3756a --- /dev/null +++ b/psync/src/test/fixtures/android_app/src/main/res/xml/prefs.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/psync/src/test/groovy/com/flipboard/psync/PsyncTest.groovy b/psync/src/test/groovy/com/flipboard/psync/PsyncTest.groovy new file mode 100644 index 0000000..697369d --- /dev/null +++ b/psync/src/test/groovy/com/flipboard/psync/PsyncTest.groovy @@ -0,0 +1,836 @@ +package com.flipboard.psync +import com.android.build.gradle.api.ApplicationVariant +import com.android.build.gradle.api.LibraryVariant +import com.github.javaparser.JavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.body.BodyDeclaration +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration +import com.github.javaparser.ast.body.ConstructorDeclaration +import com.github.javaparser.ast.body.FieldDeclaration +import com.github.javaparser.ast.body.MethodDeclaration +import com.github.javaparser.ast.stmt.ReturnStmt +import org.gradle.api.Project +import org.gradle.api.ProjectConfigurationException +import org.gradle.api.Task +import org.gradle.testfixtures.ProjectBuilder +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.Test +import rx.Observable + +import java.lang.reflect.Modifier + +import static com.flipboard.psync.TestHelper.isPSF +import static com.google.common.truth.Truth.assertThat + +class PsyncTest { + + // This is necessary because the IDE debugger and command line invocations have different working directories ಠ_ಠ + private static final WORKING_DIR = System.getProperty("user.dir") + private static final PATH_PREFIX = WORKING_DIR.endsWith("psync") ? WORKING_DIR : "$WORKING_DIR/psync" + + static final String FIXTURE_WORKING_DIR = "$PATH_PREFIX/src/test/fixtures/android_app" + private static final RESOURCE_PATH = "$PATH_PREFIX/src/test/resources/" + private static final OUT_PATH = "$PATH_PREFIX/build/test/out/" + + @BeforeClass + static void setup() { + File destinationDir = new File(OUT_PATH) + destinationDir.mkdirs() + } + + @AfterClass + static void tearDown() { + new File(OUT_PATH).deleteDir() + + // I don't know why this gets generated, but toss it + new File("${FIXTURE_WORKING_DIR}/userHome").deleteDir() + } + + @Test + void testBasicPrefEntry() { + PrefEntry entry = PrefEntry.create("myKey", "someDefault") + assertThat(entry.equals(entry)) // Truth will check == under the hood first, which we want to avoid + assertThat(entry).isNotEqualTo "Banana" + + PrefEntry that = PrefEntry.create("myKey", "someDefault") + assertThat(entry).isEqualTo that + + that = PrefEntry.create("myKey", "differentDefault") + assertThat(entry).isEqualTo that + + that = PrefEntry.create("myKey", 3) + assertThat(entry).isEqualTo that + + that = PrefEntry.create("myOtherKey", "someDefault") + assertThat(entry).isNotEqualTo that + } + + @Test(expected = UnsupportedOperationException.class) + void testThrowsOnUnsupportedType() { + PrefEntry.create("myKey", Double.class) + } + + @Test() + void testUnsupportedResourceType() { + assertThat(PSyncTask.generateResourcePrefEntry("my_key", "@id/my_pref_value").isBlank()) + assertThat(PSyncTask.generateResourcePrefEntry("my_key", "@dimen/my_pref_value").isBlank()) + assertThat(PSyncTask.generateResourcePrefEntry("my_key", "@banana/my_pref_value").isBlank()) + } + + @Test + void testGenerateResourcePrefEntry() { + PrefEntry entry = PSyncTask.generateResourcePrefEntry("my_key", "@string/my_pref_value") + assertThat(entry.key).isEqualTo "my_key" + assertThat(entry.isResource) + assertThat(entry.defaultValue).isInstanceOf String + assertThat(entry.defaultValue).isEqualTo "my_pref_value" + assertThat(entry.resType).isEqualTo "string" + assertThat(entry.defaultType).isEqualTo String.class + } + + @Test + void testNaturalOrdering() { + List entries = Arrays.asList( + PrefEntry.create("a", "someDefault"), + PrefEntry.create("b", "someDefault"), + PrefEntry.create("c", "someDefault"), + PrefEntry.create("d", "someDefault"), + ) + + Collections.shuffle(entries) + Collections.sort(entries) + assertThat(entries).isStrictlyOrdered() + } + + @Test + void testCamelKey() { + assertThat(PClassGenerator.camelCaseKey("lower_case")).isEqualTo "lowerCase" + assertThat(PClassGenerator.camelCaseKey("camelCase")).isEqualTo "camelCase" + assertThat(PClassGenerator.camelCaseKey("UPPER_STUFF")).isEqualTo "upperStuff" + assertThat(PClassGenerator.camelCaseKey("UPPER-STUFF")).isEqualTo "upperStuff" + assertThat(PClassGenerator.camelCaseKey("CamelCase")).isEqualTo "camelCase" + assertThat(PClassGenerator.camelCaseKey("oneTwo_3")).isEqualTo "onetwo3" + } + + @Test + void testGetPrefEntriesFromFiles() { + RecordingObserver o = new RecordingObserver<>() + //noinspection GroovyAssignabilityCheck + PSyncTask.getPrefEntriesFromFiles(Collections.singletonList(new File("$RESOURCE_PATH/prefs.xml"))) + .flatMap {List entries -> Observable.from(entries)} + .subscribe(o) + + PrefEntry entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "number_of_columns" + assertThat(entry.defaultValue).isInstanceOf Integer + assertThat(entry.defaultType).isEqualTo int.class + assertThat(!entry.isResource) + assertThat(entry.defaultValue).isEqualTo 3 + assertThat(entry.resType).isNull() + assertThat(entry.valueType).isEqualTo int.class + assertThat(entry.resourceDefaultValueGetterStmt).isNull() + assertThat(entry.hasListAttributes).isFalse() + assertThat(entry.entriesGetterStmt).isNull() + assertThat(entry.entryValuesGetterStmt).isNull() + + entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "number_of_rows" + assertThat(entry.defaultValue).isInstanceOf String + assertThat(entry.defaultType).isEqualTo String.class + assertThat(entry.isResource) + assertThat(entry.defaultValue).isEqualTo "num_rows" + assertThat(entry.resType).isEqualTo "integer" + assertThat(entry.valueType).isEqualTo int.class + assertThat(entry.resourceDefaultValueGetterStmt).isEqualTo "getInteger(defaultResId)" + assertThat(entry.hasListAttributes).isFalse() + assertThat(entry.entriesGetterStmt).isNull() + assertThat(entry.entryValuesGetterStmt).isNull() + + entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "pref_cat_server" + assertThat(entry.defaultValue).isNull() + assertThat(entry.defaultType).isNull() + assertThat(!entry.isResource) + assertThat(entry.resType).isNull() + assertThat(entry.resourceDefaultValueGetterStmt).isNull() + assertThat(entry.hasListAttributes).isFalse() + assertThat(entry.entriesGetterStmt).isNull() + assertThat(entry.entryValuesGetterStmt).isNull() + + entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "primary_color" + assertThat(entry.defaultValue).isInstanceOf String + assertThat(entry.defaultType).isEqualTo String.class + assertThat(entry.isResource) + assertThat(entry.defaultValue).isEqualTo "flipboard_red" + assertThat(entry.resType).isEqualTo "color" + assertThat(entry.valueType).isEqualTo int.class + assertThat(entry.resourceDefaultValueGetterStmt).isEqualTo "getColor(defaultResId)" + assertThat(entry.hasListAttributes).isFalse() + assertThat(entry.entriesGetterStmt).isNull() + assertThat(entry.entryValuesGetterStmt).isNull() + + entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "request_agent" + assertThat(entry.defaultValue).isInstanceOf String + assertThat(entry.defaultType).isEqualTo String.class + assertThat(!entry.isResource) + assertThat(entry.defaultValue).isEqualTo "banana" + assertThat(entry.resType).isNull() + assertThat(entry.valueType).isEqualTo String.class + assertThat(entry.resourceDefaultValueGetterStmt).isNull() + assertThat(entry.hasListAttributes).isFalse() + assertThat(entry.entriesGetterStmt).isNull() + assertThat(entry.entryValuesGetterStmt).isNull() + + entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "request_types" + assertThat(entry.defaultValue).isInstanceOf String + assertThat(entry.defaultType).isEqualTo String.class + assertThat(entry.defaultValue).isEqualTo "default_request_type" + assertThat(entry.resType).isEqualTo "string" + assertThat(entry.isResource) + assertThat(entry.valueType).isEqualTo String.class + assertThat(entry.resourceDefaultValueGetterStmt).isEqualTo "getString(defaultResId)" + assertThat(entry.entriesGetterStmt).isEqualTo "getTextArray(R.array.request_types_entries)" + assertThat(entry.entryValuesGetterStmt).isEqualTo "getTextArray(R.array.request_types_entry_values)" + + entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "server_url" + assertThat(entry.defaultValue).isInstanceOf String + assertThat(entry.defaultType).isEqualTo String.class + assertThat(entry.isResource) + assertThat(entry.defaultValue).isEqualTo "server_url" + assertThat(entry.resType).isEqualTo "string" + assertThat(entry.valueType).isEqualTo String.class + assertThat(entry.resourceDefaultValueGetterStmt).isEqualTo "getString(defaultResId)" + assertThat(entry.hasListAttributes).isFalse() + assertThat(entry.entriesGetterStmt).isNull() + assertThat(entry.entryValuesGetterStmt).isNull() + + entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "show_images" + assertThat(entry.defaultValue).isInstanceOf Boolean + assertThat(entry.defaultType).isEqualTo boolean.class + assertThat(!entry.isResource) + assertThat(entry.defaultValue as boolean) + assertThat(entry.resType).isNull() + assertThat(entry.valueType).isEqualTo boolean.class + assertThat(entry.resourceDefaultValueGetterStmt).isNull() + assertThat(entry.hasListAttributes).isFalse() + assertThat(entry.entriesGetterStmt).isNull() + assertThat(entry.entryValuesGetterStmt).isNull() + + entry = o.takeNext() + assertThat(entry).isNotNull() + assertThat(entry.key).isEqualTo "use_inputs" + assertThat(entry.defaultValue).isInstanceOf String.class + assertThat(entry.defaultType).isEqualTo String.class + assertThat(entry.isResource) + assertThat(entry.defaultValue).isEqualTo "use_inputs" + assertThat(entry.resType).isEqualTo "bool" + assertThat(entry.valueType).isEqualTo boolean.class + assertThat(entry.resourceDefaultValueGetterStmt).isEqualTo "getBoolean(defaultResId)" + assertThat(entry.hasListAttributes).isFalse() + assertThat(entry.entriesGetterStmt).isNull() + assertThat(entry.entryValuesGetterStmt).isNull() + + o.assertOnCompleted() + o.assertNoMoreEvents() + } + + @Test + public void testGeneration() { + List entries = PSyncTask.getPrefEntriesFromFiles(Collections.singletonList(new File("$RESOURCE_PATH/prefs.xml"))).toBlocking().first() + File outputDir = new File(OUT_PATH) + PClassGenerator.generate(entries, "com.flipboard.psync.test", outputDir, "P", true) + File generatedFile = new File("$OUT_PATH/com/flipboard/psync/test/P.java") + + assertThat(generatedFile.exists()) + + // Verify the file + CompilationUnit cu = JavaParser.parse(generatedFile) + + ClassOrInterfaceDeclaration pClass = cu.getTypes()[0] as ClassOrInterfaceDeclaration + + assertThat(Modifier.isPublic(pClass.modifiers)) + assertThat(Modifier.isFinal(pClass.modifiers)) + assertThat(pClass.name).isEqualTo "P" + assertThat(pClass.members).hasSize 15 + + ConstructorDeclaration constructor = pClass.members.find {it instanceof ConstructorDeclaration} as ConstructorDeclaration + assertThat(constructor).isNotNull() + assertThat(Modifier.isPrivate(constructor.modifiers)) + assertThat(constructor.block.stmts[0].toString()).isEqualTo "throw new AssertionError(\"No instances.\");" + assertThat(pClass.members.find {it instanceof ConstructorDeclaration}) + + List typeMembers = pClass.members.subList(6, pClass.members.size()) + typeMembers.each { + assertThat(it).isInstanceOf ClassOrInterfaceDeclaration + } + assertThat(typeMembers.collect {it.name}).isStrictlyOrdered() + + int classCount = 0; + + ClassOrInterfaceDeclaration colNum = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(colNum.modifiers)) + assertThat(colNum.name).isEqualTo "numberOfColumns" + assertThat(colNum.members).hasSize 5 + FieldDeclaration colNumKey = colNum.members[0] as FieldDeclaration + assertThat(isPSF(colNumKey.modifiers)) + assertThat(colNumKey.type.toString()).isEqualTo "String" + assertThat(colNumKey.variables[0].id.name).isEqualTo "key" + assertThat(colNumKey.variables[0].init.toString()).isEqualTo "\"number_of_columns\"" + MethodDeclaration colNumDefaultGetter = colNum.members[1] as MethodDeclaration + assertThat(isPSF(colNumDefaultGetter.modifiers)) + assertThat(colNumDefaultGetter.name).isEqualTo "defaultValue" + assertThat(colNumDefaultGetter.type.toString()).isEqualTo "int" + assertThat(colNumDefaultGetter.body.stmts).hasSize(1) + assertThat(colNumDefaultGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(colNumDefaultGetter.body.stmts[0].expr.toString()).isEqualTo "3" + MethodDeclaration colNumPrefGetter = colNum.members[2] as MethodDeclaration + assertThat(isPSF(colNumPrefGetter.modifiers)) + assertThat(colNumPrefGetter.name).isEqualTo "get" + assertThat(colNumPrefGetter.type.toString()).isEqualTo "int" + assertThat(colNumPrefGetter.body.stmts).hasSize(1) + assertThat(colNumPrefGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(colNumPrefGetter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.getInt(key, defaultValue())" + MethodDeclaration colNumPrefPutter = colNum.members[3] as MethodDeclaration + assertThat(isPSF(colNumPrefPutter.modifiers)) + assertThat(colNumPrefPutter.name).isEqualTo "put" + assertThat(colNumPrefPutter.type.toString()).isEqualTo "SharedPreferences.Editor" + assertThat(colNumPrefPutter.parameters)hasSize 1 + assertThat(Modifier.isFinal(colNumPrefPutter.parameters[0].modifiers)) + assertThat(colNumPrefPutter.parameters[0].type.toString()).isEqualTo "int" + assertThat(colNumPrefPutter.parameters[0].id.toString()).isEqualTo "val" + assertThat(colNumPrefPutter.body.stmts).hasSize(1) + assertThat(colNumPrefPutter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(colNumPrefPutter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.edit().putInt(key, val)" + MethodDeclaration colNumRxGetter = colNum.members[4] as MethodDeclaration + assertThat(isPSF(colNumRxGetter.modifiers)) + assertThat(colNumRxGetter.name).isEqualTo "rx" + assertThat(colNumRxGetter.type.toString()).isEqualTo "Preference" + assertThat(colNumRxGetter.body.stmts).hasSize(1) + assertThat(colNumRxGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(colNumRxGetter.body.stmts[0].expr.toString()).isEqualTo "RX_PREFERENCES.getInteger(key)" + + ClassOrInterfaceDeclaration numRows = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(numRows.modifiers)) + assertThat(numRows.name).isEqualTo "numberOfRows" + assertThat(numRows.members).hasSize 6 + FieldDeclaration numRowsKey = numRows.members[0] as FieldDeclaration + assertThat(isPSF(numRowsKey.modifiers)) + assertThat(numRowsKey.type.toString()).isEqualTo "String" + assertThat(numRowsKey.variables[0].id.name).isEqualTo "key" + assertThat(numRowsKey.variables[0].init.toString()).isEqualTo "\"number_of_rows\"" + FieldDeclaration numRowsDefault = numRows.members[1] as FieldDeclaration + assertThat(isPSF(numRowsDefault.modifiers)) + assertThat(numRowsDefault.type.toString()).isEqualTo "int" + assertThat(numRowsDefault.variables[0].id.name).isEqualTo "defaultResId" + assertThat(numRowsDefault.variables[0].init.toString()).isEqualTo "R.integer.num_rows" + MethodDeclaration numRowsDefaultGetter = numRows.members[2] as MethodDeclaration + assertThat(isPSF(numRowsDefaultGetter.modifiers)) + assertThat(numRowsDefaultGetter.name).isEqualTo "defaultValue" + assertThat(numRowsDefaultGetter.type.toString()).isEqualTo "int" + assertThat(numRowsDefaultGetter.body.stmts).hasSize(1) + assertThat(numRowsDefaultGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(numRowsDefaultGetter.body.stmts[0].expr.toString()).isEqualTo "RESOURCES.getInteger(defaultResId)" + MethodDeclaration numRowsPrefGetter = numRows.members[3] as MethodDeclaration + assertThat(isPSF(numRowsPrefGetter.modifiers)) + assertThat(numRowsPrefGetter.name).isEqualTo "get" + assertThat(numRowsPrefGetter.type.toString()).isEqualTo "int" + assertThat(numRowsPrefGetter.body.stmts).hasSize(1) + assertThat(numRowsPrefGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(numRowsPrefGetter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.getInt(key, defaultValue())" + MethodDeclaration numRowsPrefPutter = numRows.members[4] as MethodDeclaration + assertThat(isPSF(numRowsPrefPutter.modifiers)) + assertThat(numRowsPrefPutter.name).isEqualTo "put" + assertThat(numRowsPrefPutter.type.toString()).isEqualTo "SharedPreferences.Editor" + assertThat(numRowsPrefPutter.parameters)hasSize 1 + assertThat(Modifier.isFinal(numRowsPrefPutter.parameters[0].modifiers)) + assertThat(numRowsPrefPutter.parameters[0].type.toString()).isEqualTo "int" + assertThat(numRowsPrefPutter.parameters[0].id.toString()).isEqualTo "val" + assertThat(numRowsPrefPutter.body.stmts).hasSize(1) + assertThat(numRowsPrefPutter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(numRowsPrefPutter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.edit().putInt(key, val)" + MethodDeclaration numRowsRxGetter = numRows.members[5] as MethodDeclaration + assertThat(isPSF(numRowsRxGetter.modifiers)) + assertThat(numRowsRxGetter.name).isEqualTo "rx" + assertThat(numRowsRxGetter.type.toString()).isEqualTo "Preference" + assertThat(numRowsRxGetter.body.stmts).hasSize(1) + assertThat(numRowsRxGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(numRowsRxGetter.body.stmts[0].expr.toString()).isEqualTo "RX_PREFERENCES.getInteger(key)" + + ClassOrInterfaceDeclaration catServer = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(catServer.modifiers)) + assertThat(catServer.name).isEqualTo "prefCatServer" + assertThat(catServer.members).hasSize 1 + assertThat(catServer.members[0]).isInstanceOf FieldDeclaration + FieldDeclaration catServerField = catServer.members[0] as FieldDeclaration + assertThat(isPSF(catServerField.modifiers)) + assertThat(catServerField.type.toString()).isEqualTo "String" + assertThat(catServerField.variables[0].id.name).isEqualTo "key" + assertThat(catServerField.variables[0].init.toString()).isEqualTo "\"pref_cat_server\"" + + ClassOrInterfaceDeclaration primaryColor = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(primaryColor.modifiers)) + assertThat(primaryColor.name).isEqualTo "primaryColor" + assertThat(primaryColor.members).hasSize 6 + FieldDeclaration primaryColorKey = primaryColor.members[0] as FieldDeclaration + assertThat(isPSF(primaryColorKey.modifiers)) + assertThat(primaryColorKey.type.toString()).isEqualTo "String" + assertThat(primaryColorKey.variables[0].id.name).isEqualTo "key" + assertThat(primaryColorKey.variables[0].init.toString()).isEqualTo "\"primary_color\"" + FieldDeclaration primaryColorDefault = primaryColor.members[1] as FieldDeclaration + assertThat(isPSF(primaryColorDefault.modifiers)) + assertThat(primaryColorDefault.type.toString()).isEqualTo "int" + assertThat(primaryColorDefault.variables[0].id.name).isEqualTo "defaultResId" + assertThat(primaryColorDefault.variables[0].init.toString()).isEqualTo "R.color.flipboard_red" + MethodDeclaration primaryColorDefaultGetter = primaryColor.members[2] as MethodDeclaration + assertThat(isPSF(primaryColorDefaultGetter.modifiers)) + assertThat(primaryColorDefaultGetter.name).isEqualTo "defaultValue" + assertThat(primaryColorDefaultGetter.type.toString()).isEqualTo "int" + assertThat(primaryColorDefaultGetter.body.stmts).hasSize(1) + assertThat(primaryColorDefaultGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(primaryColorDefaultGetter.body.stmts[0].expr.toString()).isEqualTo "RESOURCES.getColor(defaultResId)" + MethodDeclaration primaryColorPrefGetter = primaryColor.members[3] as MethodDeclaration + assertThat(isPSF(primaryColorPrefGetter.modifiers)) + assertThat(primaryColorPrefGetter.name).isEqualTo "get" + assertThat(primaryColorPrefGetter.type.toString()).isEqualTo "int" + assertThat(primaryColorPrefGetter.body.stmts).hasSize(1) + assertThat(primaryColorPrefGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(primaryColorPrefGetter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.getInt(key, defaultValue())" + MethodDeclaration primaryColorPrefPutter = primaryColor.members[4] as MethodDeclaration + assertThat(isPSF(primaryColorPrefPutter.modifiers)) + assertThat(primaryColorPrefPutter.name).isEqualTo "put" + assertThat(primaryColorPrefPutter.type.toString()).isEqualTo "SharedPreferences.Editor" + assertThat(primaryColorPrefPutter.parameters)hasSize 1 + assertThat(Modifier.isFinal(primaryColorPrefPutter.parameters[0].modifiers)) + assertThat(primaryColorPrefPutter.parameters[0].type.toString()).isEqualTo "int" + assertThat(primaryColorPrefPutter.parameters[0].id.toString()).isEqualTo "val" + assertThat(primaryColorPrefPutter.body.stmts).hasSize(1) + assertThat(primaryColorPrefPutter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(primaryColorPrefPutter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.edit().putInt(key, val)" + MethodDeclaration primaryColorRxGetter = primaryColor.members[5] as MethodDeclaration + assertThat(isPSF(primaryColorRxGetter.modifiers)) + assertThat(primaryColorRxGetter.name).isEqualTo "rx" + assertThat(primaryColorRxGetter.type.toString()).isEqualTo "Preference" + assertThat(primaryColorRxGetter.body.stmts).hasSize(1) + assertThat(primaryColorRxGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(primaryColorRxGetter.body.stmts[0].expr.toString()).isEqualTo "RX_PREFERENCES.getInteger(key)" + + ClassOrInterfaceDeclaration requestAgent = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(requestAgent.modifiers)) + assertThat(requestAgent.name).isEqualTo "requestAgent" + assertThat(requestAgent.members).hasSize 5 + FieldDeclaration requestAgentKey = requestAgent.members[0] as FieldDeclaration + assertThat(isPSF(requestAgentKey.modifiers)) + assertThat(requestAgentKey.type.toString()).isEqualTo "String" + assertThat(requestAgentKey.variables[0].id.name).isEqualTo "key" + assertThat(requestAgentKey.variables[0].init.toString()).isEqualTo "\"request_agent\"" + MethodDeclaration requestAgentDefaultGetter = requestAgent.members[1] as MethodDeclaration + assertThat(isPSF(requestAgentDefaultGetter.modifiers)) + assertThat(requestAgentDefaultGetter.name).isEqualTo "defaultValue" + assertThat(requestAgentDefaultGetter.type.toString()).isEqualTo "String" + assertThat(requestAgentDefaultGetter.body.stmts).hasSize(1) + assertThat(requestAgentDefaultGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(requestAgentDefaultGetter.body.stmts[0].expr.toString()).isEqualTo "\"banana\"" + MethodDeclaration requestAgentPrefGetter = requestAgent.members[2] as MethodDeclaration + assertThat(isPSF(requestAgentPrefGetter.modifiers)) + assertThat(requestAgentPrefGetter.name).isEqualTo "get" + assertThat(requestAgentPrefGetter.type.toString()).isEqualTo "String" + assertThat(requestAgentPrefGetter.body.stmts).hasSize(1) + assertThat(requestAgentPrefGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(requestAgentPrefGetter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.getString(key, defaultValue())" + MethodDeclaration requestAgentPrefPutter = requestAgent.members[3] as MethodDeclaration + assertThat(isPSF(requestAgentPrefPutter.modifiers)) + assertThat(requestAgentPrefPutter.name).isEqualTo "put" + assertThat(requestAgentPrefPutter.type.toString()).isEqualTo "SharedPreferences.Editor" + assertThat(requestAgentPrefPutter.parameters)hasSize 1 + assertThat(Modifier.isFinal(requestAgentPrefPutter.parameters[0].modifiers)) + assertThat(requestAgentPrefPutter.parameters[0].type.toString()).isEqualTo "String" + assertThat(requestAgentPrefPutter.parameters[0].id.toString()).isEqualTo "val" + assertThat(requestAgentPrefPutter.body.stmts).hasSize(1) + assertThat(requestAgentPrefPutter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(requestAgentPrefPutter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.edit().putString(key, val)" + MethodDeclaration requestAgentRxGetter = requestAgent.members[4] as MethodDeclaration + assertThat(isPSF(requestAgentRxGetter.modifiers)) + assertThat(requestAgentRxGetter.name).isEqualTo "rx" + assertThat(requestAgentRxGetter.type.toString()).isEqualTo "Preference" + assertThat(requestAgentRxGetter.body.stmts).hasSize(1) + assertThat(requestAgentRxGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(requestAgentRxGetter.body.stmts[0].expr.toString()).isEqualTo "RX_PREFERENCES.getString(key)" + + ClassOrInterfaceDeclaration requestTypes = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(requestTypes.modifiers)) + assertThat(requestTypes.name).isEqualTo "requestTypes" + assertThat(requestTypes.members).hasSize 8 + FieldDeclaration requestTypesKey = requestTypes.members[0] as FieldDeclaration + assertThat(isPSF(requestTypesKey.modifiers)) + assertThat(requestTypesKey.type.toString()).isEqualTo "String" + assertThat(requestTypesKey.variables[0].id.name).isEqualTo "key" + assertThat(requestTypesKey.variables[0].init.toString()).isEqualTo "\"request_types\"" + FieldDeclaration requestTypesDefault = requestTypes.members[1] as FieldDeclaration + assertThat(isPSF(requestTypesDefault.modifiers)) + assertThat(requestTypesDefault.type.toString()).isEqualTo "int" + assertThat(requestTypesDefault.variables[0].id.name).isEqualTo "defaultResId" + assertThat(requestTypesDefault.variables[0].init.toString()).isEqualTo "R.string.default_request_type" + MethodDeclaration requestTypesDefaultGetter = requestTypes.members[2] as MethodDeclaration + assertThat(isPSF(requestTypesDefaultGetter.modifiers)) + assertThat(requestTypesDefaultGetter.name).isEqualTo "defaultValue" + assertThat(requestTypesDefaultGetter.type.toString()).isEqualTo "String" + assertThat(requestTypesDefaultGetter.body.stmts).hasSize(1) + assertThat(requestTypesDefaultGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(requestTypesDefaultGetter.body.stmts[0].expr.toString()).isEqualTo "RESOURCES.getString(defaultResId)" + MethodDeclaration requestTypesPrefGetter = requestTypes.members[3] as MethodDeclaration + assertThat(isPSF(requestTypesPrefGetter.modifiers)) + assertThat(requestTypesPrefGetter.name).isEqualTo "get" + assertThat(requestTypesPrefGetter.type.toString()).isEqualTo "String" + assertThat(requestTypesPrefGetter.body.stmts).hasSize(1) + assertThat(requestTypesPrefGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(requestTypesPrefGetter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.getString(key, defaultValue())" + MethodDeclaration requestTypesPrefPutter = requestTypes.members[4] as MethodDeclaration + assertThat(isPSF(requestTypesPrefPutter.modifiers)) + assertThat(requestTypesPrefPutter.name).isEqualTo "put" + assertThat(requestTypesPrefPutter.type.toString()).isEqualTo "SharedPreferences.Editor" + assertThat(requestTypesPrefPutter.parameters)hasSize 1 + assertThat(Modifier.isFinal(requestTypesPrefPutter.parameters[0].modifiers)) + assertThat(requestTypesPrefPutter.parameters[0].type.toString()).isEqualTo "String" + assertThat(requestTypesPrefPutter.parameters[0].id.toString()).isEqualTo "val" + assertThat(requestTypesPrefPutter.body.stmts).hasSize(1) + assertThat(requestTypesPrefPutter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(requestTypesPrefPutter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.edit().putString(key, val)" + MethodDeclaration requestTypesRxGetter = requestTypes.members[5] as MethodDeclaration + assertThat(isPSF(requestTypesRxGetter.modifiers)) + assertThat(requestTypesRxGetter.name).isEqualTo "rx" + assertThat(requestTypesRxGetter.type.toString()).isEqualTo "Preference" + assertThat(requestTypesRxGetter.body.stmts).hasSize(1) + assertThat(requestTypesRxGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(requestTypesRxGetter.body.stmts[0].expr.toString()).isEqualTo "RX_PREFERENCES.getString(key)" + MethodDeclaration entriesGetter = requestTypes.members[6] as MethodDeclaration + assertThat(isPSF(entriesGetter.modifiers)) + assertThat(entriesGetter.name).isEqualTo "entries" + assertThat(entriesGetter.type.toString()).isEqualTo "CharSequence[]" + assertThat(entriesGetter.body.stmts).hasSize(1) + assertThat(entriesGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(entriesGetter.body.stmts[0].expr.toString()).isEqualTo "RESOURCES.getTextArray(R.array.request_types_entries)" + MethodDeclaration entryValuesGetter = requestTypes.members[7] as MethodDeclaration + assertThat(isPSF(entryValuesGetter.modifiers)) + assertThat(entryValuesGetter.name).isEqualTo "entryValues" + assertThat(entryValuesGetter.type.toString()).isEqualTo "CharSequence[]" + assertThat(entryValuesGetter.body.stmts).hasSize(1) + assertThat(entryValuesGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(entryValuesGetter.body.stmts[0].expr.toString()).isEqualTo "RESOURCES.getTextArray(R.array.request_types_entry_values)" + + ClassOrInterfaceDeclaration serverUrl = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(serverUrl.modifiers)) + assertThat(serverUrl.name).isEqualTo "serverUrl" + assertThat(serverUrl.members).hasSize 6 + FieldDeclaration serverUrlKey = serverUrl.members[0] as FieldDeclaration + assertThat(isPSF(serverUrlKey.modifiers)) + assertThat(serverUrlKey.type.toString()).isEqualTo "String" + assertThat(serverUrlKey.variables[0].id.name).isEqualTo "key" + assertThat(serverUrlKey.variables[0].init.toString()).isEqualTo "\"server_url\"" + FieldDeclaration serverUrlDefault = serverUrl.members[1] as FieldDeclaration + assertThat(isPSF(serverUrlDefault.modifiers)) + assertThat(serverUrlDefault.type.toString()).isEqualTo "int" + assertThat(serverUrlDefault.variables[0].id.name).isEqualTo "defaultResId" + assertThat(serverUrlDefault.variables[0].init.toString()).isEqualTo "R.string.server_url" + MethodDeclaration serverDefaultGetter = serverUrl.members[2] as MethodDeclaration + assertThat(isPSF(serverDefaultGetter.modifiers)) + assertThat(serverDefaultGetter.name).isEqualTo "defaultValue" + assertThat(serverDefaultGetter.type.toString()).isEqualTo "String" + assertThat(serverDefaultGetter.body.stmts).hasSize(1) + assertThat(serverDefaultGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(serverDefaultGetter.body.stmts[0].expr.toString()).isEqualTo "RESOURCES.getString(defaultResId)" + MethodDeclaration serverUrlPrefGetter = serverUrl.members[3] as MethodDeclaration + assertThat(isPSF(serverUrlPrefGetter.modifiers)) + assertThat(serverUrlPrefGetter.name).isEqualTo "get" + assertThat(serverUrlPrefGetter.type.toString()).isEqualTo "String" + assertThat(serverUrlPrefGetter.body.stmts).hasSize(1) + assertThat(serverUrlPrefGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(serverUrlPrefGetter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.getString(key, defaultValue())" + MethodDeclaration serverUrlPrefPutter = serverUrl.members[4] as MethodDeclaration + assertThat(isPSF(serverUrlPrefPutter.modifiers)) + assertThat(serverUrlPrefPutter.name).isEqualTo "put" + assertThat(serverUrlPrefPutter.type.toString()).isEqualTo "SharedPreferences.Editor" + assertThat(serverUrlPrefPutter.parameters)hasSize 1 + assertThat(Modifier.isFinal(serverUrlPrefPutter.parameters[0].modifiers)) + assertThat(serverUrlPrefPutter.parameters[0].type.toString()).isEqualTo "String" + assertThat(serverUrlPrefPutter.parameters[0].id.toString()).isEqualTo "val" + assertThat(serverUrlPrefPutter.body.stmts).hasSize(1) + assertThat(serverUrlPrefPutter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(serverUrlPrefPutter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.edit().putString(key, val)" + MethodDeclaration serverUrlRxGetter = serverUrl.members[5] as MethodDeclaration + assertThat(isPSF(serverUrlRxGetter.modifiers)) + assertThat(serverUrlRxGetter.name).isEqualTo "rx" + assertThat(serverUrlRxGetter.type.toString()).isEqualTo "Preference" + assertThat(serverUrlRxGetter.body.stmts).hasSize(1) + assertThat(serverUrlRxGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(serverUrlRxGetter.body.stmts[0].expr.toString()).isEqualTo "RX_PREFERENCES.getString(key)" + + ClassOrInterfaceDeclaration showImages = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(showImages.modifiers)) + assertThat(showImages.name).isEqualTo "showImages" + assertThat(showImages.members).hasSize 5 + assertThat(showImages.members[0]).isInstanceOf FieldDeclaration + FieldDeclaration showImagesKey = showImages.members[0] as FieldDeclaration + assertThat(isPSF(showImagesKey.modifiers)) + assertThat(showImagesKey.type.toString()).isEqualTo "String" + assertThat(showImagesKey.variables[0].id.name).isEqualTo "key" + assertThat(showImagesKey.variables[0].init.toString()).isEqualTo "\"show_images\"" + MethodDeclaration showImagesDefaultGetter = showImages.members[1] as MethodDeclaration + assertThat(isPSF(showImagesDefaultGetter.modifiers)) + assertThat(showImagesDefaultGetter.type.toString()).isEqualTo "boolean" + assertThat(showImagesDefaultGetter.name).isEqualTo "defaultValue" + assertThat(showImagesDefaultGetter.body.stmts).hasSize(1) + assertThat(showImagesDefaultGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(showImagesDefaultGetter.body.stmts[0].expr.toString()).isEqualTo "true" + MethodDeclaration showImagesPrefGetter = showImages.members[2] as MethodDeclaration + assertThat(isPSF(showImagesPrefGetter.modifiers)) + assertThat(showImagesPrefGetter.name).isEqualTo "get" + assertThat(showImagesPrefGetter.type.toString()).isEqualTo "boolean" + assertThat(showImagesPrefGetter.body.stmts).hasSize(1) + assertThat(showImagesPrefGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(showImagesPrefGetter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.getBoolean(key, defaultValue())" + MethodDeclaration showImagesPrefPutter = showImages.members[3] as MethodDeclaration + assertThat(isPSF(showImagesPrefPutter.modifiers)) + assertThat(showImagesPrefPutter.name).isEqualTo "put" + assertThat(showImagesPrefPutter.type.toString()).isEqualTo "SharedPreferences.Editor" + assertThat(showImagesPrefPutter.parameters)hasSize 1 + assertThat(Modifier.isFinal(showImagesPrefPutter.parameters[0].modifiers)) + assertThat(showImagesPrefPutter.parameters[0].type.toString()).isEqualTo "boolean" + assertThat(showImagesPrefPutter.parameters[0].id.toString()).isEqualTo "val" + assertThat(showImagesPrefPutter.body.stmts).hasSize(1) + assertThat(showImagesPrefPutter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(showImagesPrefPutter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.edit().putBoolean(key, val)" + MethodDeclaration showImagesRxGetter = showImages.members[4] as MethodDeclaration + assertThat(isPSF(showImagesRxGetter.modifiers)) + assertThat(showImagesRxGetter.name).isEqualTo "rx" + assertThat(showImagesRxGetter.type.toString()).isEqualTo "Preference" + assertThat(showImagesRxGetter.body.stmts).hasSize(1) + assertThat(showImagesRxGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(showImagesRxGetter.body.stmts[0].expr.toString()).isEqualTo "RX_PREFERENCES.getBoolean(key)" + + ClassOrInterfaceDeclaration useInputs = typeMembers[classCount++] as ClassOrInterfaceDeclaration + assertThat(isPSF(useInputs.modifiers)) + assertThat(useInputs.name).isEqualTo "useInputs" + assertThat(useInputs.members).hasSize 6 + FieldDeclaration useInputsKey = useInputs.members[0] as FieldDeclaration + assertThat(isPSF(useInputsKey.modifiers)) + assertThat(useInputsKey.type.toString()).isEqualTo "String" + assertThat(useInputsKey.variables[0].id.name).isEqualTo "key" + assertThat(useInputsKey.variables[0].init.toString()).isEqualTo "\"use_inputs\"" + FieldDeclaration useInputsDefault = useInputs.members[1] as FieldDeclaration + assertThat(isPSF(useInputsDefault.modifiers)) + assertThat(useInputsDefault.type.toString()).isEqualTo "int" + assertThat(useInputsDefault.variables[0].id.name).isEqualTo "defaultResId" + assertThat(useInputsDefault.variables[0].init.toString()).isEqualTo "R.bool.use_inputs" + MethodDeclaration useInputsDefaultGetter = useInputs.members[2] as MethodDeclaration + assertThat(isPSF(useInputsDefaultGetter.modifiers)) + assertThat(useInputsDefaultGetter.name).isEqualTo "defaultValue" + assertThat(useInputsDefaultGetter.type.toString()).isEqualTo "boolean" + assertThat(useInputsDefaultGetter.body.stmts).hasSize(1) + assertThat(useInputsDefaultGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(useInputsDefaultGetter.body.stmts[0].expr.toString()).isEqualTo "RESOURCES.getBoolean(defaultResId)" + MethodDeclaration useInputsPrefGetter = useInputs.members[3] as MethodDeclaration + assertThat(isPSF(useInputsPrefGetter.modifiers)) + assertThat(useInputsPrefGetter.name).isEqualTo "get" + assertThat(useInputsPrefGetter.type.toString()).isEqualTo "boolean" + assertThat(useInputsPrefGetter.body.stmts).hasSize(1) + assertThat(useInputsPrefGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(useInputsPrefGetter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.getBoolean(key, defaultValue())" + MethodDeclaration useInputsPrefPutter = useInputs.members[4] as MethodDeclaration + assertThat(isPSF(useInputsPrefPutter.modifiers)) + assertThat(useInputsPrefPutter.name).isEqualTo "put" + assertThat(useInputsPrefPutter.type.toString()).isEqualTo "SharedPreferences.Editor" + assertThat(useInputsPrefPutter.parameters)hasSize 1 + assertThat(Modifier.isFinal(useInputsPrefPutter.parameters[0].modifiers)) + assertThat(useInputsPrefPutter.parameters[0].type.toString()).isEqualTo "boolean" + assertThat(useInputsPrefPutter.parameters[0].id.toString()).isEqualTo "val" + assertThat(useInputsPrefPutter.body.stmts).hasSize(1) + assertThat(useInputsPrefPutter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(useInputsPrefPutter.body.stmts[0].expr.toString()).isEqualTo "PREFERENCES.edit().putBoolean(key, val)" + MethodDeclaration useInputsRxGetter = useInputs.members[5] as MethodDeclaration + assertThat(isPSF(useInputsRxGetter.modifiers)) + assertThat(useInputsRxGetter.name).isEqualTo "rx" + assertThat(useInputsRxGetter.type.toString()).isEqualTo "Preference" + assertThat(useInputsRxGetter.body.stmts).hasSize(1) + assertThat(useInputsRxGetter.body.stmts[0]).isInstanceOf ReturnStmt + assertThat(useInputsRxGetter.body.stmts[0].expr.toString()).isEqualTo "RX_PREFERENCES.getBoolean(key)" + + // Cleanup + outputDir.deleteDir() + } + + @Test + public void testBasicConfiguration() { + Project project = TestHelper.evaluatableAppProject() + PSyncPlugin plugin = new PSyncPlugin() + plugin.apply(project) + project.evaluate() + + // Register our task with the variant + project.android.applicationVariants.all { ApplicationVariant variant -> + Task task = project.tasks."generatePrefKeysFor${variant.name.capitalize()}" + assertThat(task).isNotNull() + + PSyncTask syncTask = task as PSyncTask + assertThat(syncTask.packageName).isEqualTo "com.flipboard.psync.test" + assertThat(syncTask.className).isEqualTo "P" + assertThat(syncTask.outputDir).isEqualTo new File("$project.buildDir/generated/source/psync/$variant.flavorName/$variant.buildType.name/") + + List xmlFiles = syncTask.getSource().collect{it} + assertThat(xmlFiles).isNotNull() + assertThat(xmlFiles).hasSize 1 + assertThat(xmlFiles[0]).isEqualTo new File("${FIXTURE_WORKING_DIR}/src/main/res/xml/prefs.xml") + + syncTask.generate(TestHelper.getTaskInputs()) + + assertThat(syncTask.outputDir.exists()) + assertThat("${syncTask.outputDir}/{${syncTask.packageName.replace('.', '/')}/${syncTask.className}.java") + } + + project.buildDir.deleteDir() + } + + @Test + public void testClearExistingDir() { + Project project = TestHelper.evaluatableAppProject() + PSyncPlugin plugin = new PSyncPlugin() + plugin.apply(project) + project.evaluate() + + // Register our task with the variant + project.android.applicationVariants.all { ApplicationVariant variant -> + Task task = project.tasks."generatePrefKeysFor${variant.name.capitalize()}" + assertThat(task).isNotNull() + + PSyncTask syncTask = task as PSyncTask + File outputDir = syncTask.outputDir; + outputDir.mkdirs() + File tmpFile = new File(outputDir, "tmp.txt") + tmpFile.createNewFile() + + syncTask.generate(TestHelper.getTaskInputs()) + + assertThat(outputDir.exists()) + assertThat(outputDir.listFiles().collect {it.name}).doesNotContain("tmp.txt") + } + + project.buildDir.deleteDir() + } + + @Test + public void testBasicLibConfiguration() { + Project project = TestHelper.evaluatableLibProject() + PSyncPlugin plugin = new PSyncPlugin() + plugin.apply(project) + project.psync { + packageName = 'com.flipboard.psync.test' + } + project.evaluate() + + // Register our task with the variant + project.android.libraryVariants.all { LibraryVariant variant -> + Task task = project.tasks."generatePrefKeysFor${variant.name.capitalize()}" + assertThat(task).isNotNull() + + PSyncTask syncTask = task as PSyncTask + assertThat(syncTask.packageName).isEqualTo "com.flipboard.psync.test" + assertThat(syncTask.className).isEqualTo "P" + assertThat(syncTask.outputDir).isEqualTo new File("$project.buildDir/generated/source/psync/$variant.flavorName/$variant.buildType.name/") + + List xmlFiles = syncTask.getSource().collect{it} + assertThat(xmlFiles).isNotNull() + assertThat(xmlFiles).hasSize 1 + assertThat(xmlFiles[0]).isEqualTo new File("${FIXTURE_WORKING_DIR}/src/main/res/xml/prefs.xml") + + syncTask.generate(TestHelper.getTaskInputs()) + + assertThat(syncTask.outputDir.exists()) + assertThat("${syncTask.outputDir}/{${syncTask.packageName.replace('.', '/')}/${syncTask.className}.java") + } + + project.buildDir.deleteDir() + } + + @Test(expected = ProjectConfigurationException.class) + public void testThrowsOnMissingAppId() { + Project project = ProjectBuilder.builder().withProjectDir(new File(FIXTURE_WORKING_DIR)).build() + PSyncPlugin plugin = new PSyncPlugin() + plugin.apply(project) + project.evaluate() + // No plugin applied, can't work here. + } + + @Test(expected = ProjectConfigurationException.class) + public void testThrowsOnMissingLibPackage() { + Project project = TestHelper.evaluatableLibProject() + PSyncPlugin plugin = new PSyncPlugin() + plugin.apply(project) + project.evaluate() + // No package name, can't work here. + } + + @Test + public void testIncludes() { + Project project = TestHelper.evaluatableAppProject() + PSyncPlugin plugin = new PSyncPlugin() + plugin.apply(project) + project.psync { + includesPattern = '**/xml/otherprefs.xml' + } + project.evaluate() + + // Register our task with the variant + project.android.applicationVariants.all { ApplicationVariant variant -> + Task task = project.tasks."generatePrefKeysFor${variant.name.capitalize()}" + assertThat(task).isNotNull() + + PSyncTask syncTask = task as PSyncTask + List xmlFiles = syncTask.getSource().collect{it} + assertThat(xmlFiles).isNotNull() + assertThat(xmlFiles).isEmpty() + } + + project.buildDir.deleteDir() + } + + @Test + public void testClassName() { + String name = "MyPrefs" + Project project = TestHelper.evaluatableAppProject() + PSyncPlugin plugin = new PSyncPlugin() + plugin.apply(project) + project.psync { + className = name + } + project.evaluate() + + // Register our task with the variant + project.android.applicationVariants.all { ApplicationVariant variant -> + Task task = project.tasks."generatePrefKeysFor${variant.name.capitalize()}" + assertThat(task).isNotNull() + + PSyncTask syncTask = task as PSyncTask + assertThat(syncTask.className).isEqualTo name + + syncTask.generate(TestHelper.getTaskInputs()) + + assertThat(syncTask.outputDir.exists()) + assertThat("${syncTask.outputDir}/{${syncTask.packageName.replace('.', '/')}/${name}.java") + } + + project.buildDir.deleteDir() + } +} diff --git a/psync/src/test/groovy/com/flipboard/psync/RecordingObserver.groovy b/psync/src/test/groovy/com/flipboard/psync/RecordingObserver.groovy new file mode 100644 index 0000000..c6521c1 --- /dev/null +++ b/psync/src/test/groovy/com/flipboard/psync/RecordingObserver.groovy @@ -0,0 +1,102 @@ +package com.flipboard.psync + +import rx.Observer + +import java.util.concurrent.BlockingDeque +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.TimeUnit + +import static com.google.common.truth.Truth.assertThat + +public final class RecordingObserver implements Observer { + private static final String TAG = "RecordingObserver"; + + private final BlockingDeque events = new LinkedBlockingDeque(); + + @Override + public void onCompleted() { + println "onCompleted" + events.addLast(new OnCompleted()); + } + + @Override + public void onError(Throwable e) { + println "onError - $e" + events.addLast(new OnError(e)); + } + + @Override + public void onNext(T t) { + println "onNext - $t" + events.addLast(new OnNext(t)); + } + + private E takeEvent(Class wanted) { + Object event; + try { + event = events.pollFirst(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + if (event == null) { + throw new NoSuchElementException( + "No event found while waiting for " + wanted.getSimpleName()); + } + assertThat(event).isInstanceOf(wanted); + return wanted.cast(event); + } + + public T takeNext() { + OnNext event = takeEvent(OnNext.class); + return event.value; + } + + public Throwable takeError() { + return takeEvent(OnError.class).throwable; + } + + public void assertOnCompleted() { + takeEvent(OnCompleted.class); + } + + public void assertNoMoreEvents() { + try { + Object event = takeEvent(Object.class); + throw new IllegalStateException("Expected no more events but got " + event); + } catch (NoSuchElementException ignored) { + } + } + + private final class OnNext { + final T value; + + private OnNext(T value) { + this.value = value; + } + + @Override + public String toString() { + return "OnNext[$value]"; + } + } + + private final class OnCompleted { + @Override + public String toString() { + return "OnCompleted"; + } + } + + private final class OnError { + private final Throwable throwable; + + private OnError(Throwable throwable) { + this.throwable = throwable; + } + + @Override + public String toString() { + return "OnError[$throwable]"; + } + } +} diff --git a/psync/src/test/groovy/com/flipboard/psync/TestHelper.groovy b/psync/src/test/groovy/com/flipboard/psync/TestHelper.groovy new file mode 100644 index 0000000..6491229 --- /dev/null +++ b/psync/src/test/groovy/com/flipboard/psync/TestHelper.groovy @@ -0,0 +1,84 @@ +package com.flipboard.psync + +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.tasks.incremental.IncrementalTaskInputs +import org.gradle.api.tasks.incremental.InputFileDetails +import org.gradle.testfixtures.ProjectBuilder + +import java.lang.reflect.Modifier + +final class TestHelper { + + public static Project evaluatableAppProject() { + Project project = ProjectBuilder.builder().withProjectDir(new File(PsyncTest.FIXTURE_WORKING_DIR)).build() + project.apply plugin: 'com.android.application' + project.android { + compileSdkVersion 23 + buildToolsVersion '23.0.0' + + defaultConfig { + versionCode 1 + versionName '1.0' + minSdkVersion 14 + targetSdkVersion 23 + applicationId 'com.flipboard.psync.test' + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } + } + + return project + } + + public static Project evaluatableLibProject() { + Project project = ProjectBuilder.builder().withProjectDir(new File(PsyncTest.FIXTURE_WORKING_DIR)).build() + project.apply plugin: 'com.android.library' + project.android { + compileSdkVersion 23 + buildToolsVersion '23.0.0' + + defaultConfig { + versionCode 1 + versionName '1.0' + minSdkVersion 14 + targetSdkVersion 23 + } + + buildTypes { + release { + signingConfig signingConfigs.debug + } + } + } + + return project + } + + public static IncrementalTaskInputs getTaskInputs() { + return new IncrementalTaskInputs() { + @Override + boolean isIncremental() { + return false + } + + @Override + void outOfDate(Action action) { + + } + + @Override + void removed(Action action) { + + } + } + } + + public static boolean isPSF(int modifiers) { + return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) + } +} diff --git a/psync/src/test/resources/prefs.xml b/psync/src/test/resources/prefs.xml new file mode 100644 index 0000000..cbe2a27 --- /dev/null +++ b/psync/src/test/resources/prefs.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..f2ea6fa --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,38 @@ +apply plugin: 'com.android.application' +apply plugin: 'com.flipboard.psync' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.0" + + defaultConfig { + applicationId "com.flipboard.psyncsample" + minSdkVersion 14 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +psync { + includesPattern = "**/xml/prefs_*.xml" + generateRx = true +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:23.0.0' + compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.0' + + androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2' + androidTestCompile 'com.android.support.test:runner:0.3' + androidTestCompile 'com.android.support:support-annotations:23.0.0' +} diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..0f21d1b --- /dev/null +++ b/sample/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/hsweers/dev/android/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/sample/src/androidTest/java/com/flipboard/psyncsample/MainActivityEspressoTest.java b/sample/src/androidTest/java/com/flipboard/psyncsample/MainActivityEspressoTest.java new file mode 100644 index 0000000..14e6467 --- /dev/null +++ b/sample/src/androidTest/java/com/flipboard/psyncsample/MainActivityEspressoTest.java @@ -0,0 +1,26 @@ +package com.flipboard.psyncsample; + +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; + +@RunWith(AndroidJUnit4.class) +public class MainActivityEspressoTest { + + @Rule + public ActivityTestRule activityRule = new ActivityTestRule<>(MainActivity.class); + + @Test + public void displayInformationTest() { + onView(withId(R.id.display)).check(matches(isDisplayed())); + } + +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2e5722c --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/sample/src/main/java/com/flipboard/psyncsample/MainActivity.java b/sample/src/main/java/com/flipboard/psyncsample/MainActivity.java new file mode 100644 index 0000000..eb3a86c --- /dev/null +++ b/sample/src/main/java/com/flipboard/psyncsample/MainActivity.java @@ -0,0 +1,38 @@ +package com.flipboard.psyncsample; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_settings) { + return true; + } + + return super.onOptionsItemSelected(item); + } +} diff --git a/sample/src/main/java/com/flipboard/psyncsample/MainActivityFragment.java b/sample/src/main/java/com/flipboard/psyncsample/MainActivityFragment.java new file mode 100644 index 0000000..55d83ca --- /dev/null +++ b/sample/src/main/java/com/flipboard/psyncsample/MainActivityFragment.java @@ -0,0 +1,87 @@ +package com.flipboard.psyncsample; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +/** + * A placeholder fragment containing a simple view. + */ +public class MainActivityFragment extends Fragment { + + private static final String NEWLINE = "\n"; + private static final String TAB = "\t"; + + public MainActivityFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_main, container, false); + + // Normally you would do this in your Application class + P.init(getActivity().getApplication()); + + TextView display = (TextView) view.findViewById(R.id.display); + + String text = new StringBuilder() + .append("Server category:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.categoryServer.key).append(NEWLINE) + .append(NEWLINE) + .append("Number of columns:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.numberOfColumns.key).append(NEWLINE) + .append(TAB).append("defaultValue: ").append(P.numberOfColumns.defaultValue()).append(NEWLINE) + .append(NEWLINE) + .append("Number of rows:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.numberOfRows.key).append(NEWLINE) + .append(TAB).append("defaultResId: ").append(P.numberOfRows.defaultResId).append(NEWLINE) + .append(TAB).append("defaultValue: ").append(P.numberOfRows.defaultValue()).append(NEWLINE) + .append(NEWLINE) + .append("Primary color:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.primaryColor.key).append(NEWLINE) + .append(TAB).append("defaultResId: ").append(P.primaryColor.defaultResId).append(NEWLINE) + .append(TAB).append("defaultValue: ").append(P.primaryColor.defaultValue()).append(NEWLINE) + .append(NEWLINE) + .append("Request agent:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.requestAgent.key).append(NEWLINE) + .append(TAB).append("defaultValue: ").append(P.requestAgent.defaultValue()).append(NEWLINE) + .append(NEWLINE) + .append("Request types:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.requestTypes.key).append(NEWLINE) + .append(TAB).append("defaultResId: ").append(P.requestTypes.defaultResId).append(NEWLINE) + .append(TAB).append("defaultValue: ").append(P.requestTypes.defaultValue()).append(NEWLINE) + .append(NEWLINE) + .append("Server url:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.serverUrl.key).append(NEWLINE) + .append(TAB).append("defaultResId: ").append(P.serverUrl.defaultResId).append(NEWLINE) + .append(TAB).append("defaultValue: ").append(P.serverUrl.defaultValue()).append(NEWLINE) + .append(NEWLINE) + .append("Show images:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.showImages.key).append(NEWLINE) + .append(TAB).append("defaultResId: ").append(P.showImages.defaultValue()).append(NEWLINE) + .append(NEWLINE) + .append("Use inputs:") + .append(NEWLINE) + .append(TAB).append("key: ").append(P.useInputs.key).append(NEWLINE) + .append(TAB).append("defaultResId: ").append(P.useInputs.defaultResId).append(NEWLINE) + .append(TAB).append("defaultValue: ").append(P.useInputs.defaultValue()).append(NEWLINE) + .append(NEWLINE) + .toString(); + + display.setVisibility(View.VISIBLE); + display.setText(text); + return view; + } +} diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..0c67277 --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,7 @@ + diff --git a/sample/src/main/res/layout/fragment_main.xml b/sample/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000..5e5546d --- /dev/null +++ b/sample/src/main/res/layout/fragment_main.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/sample/src/main/res/menu/menu_main.xml b/sample/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..a459e0a --- /dev/null +++ b/sample/src/main/res/menu/menu_main.xml @@ -0,0 +1,9 @@ + + + diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..cde69bc Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c133a0c Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..bfa42f0 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..324e72c Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/values-w820dp/dimens.xml b/sample/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..63fc816 --- /dev/null +++ b/sample/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/sample/src/main/res/values/arrays.xml b/sample/src/main/res/values/arrays.xml new file mode 100644 index 0000000..ee09ab8 --- /dev/null +++ b/sample/src/main/res/values/arrays.xml @@ -0,0 +1,19 @@ + + + + POST + GET + + + POST + GET + DELETE + PUT + + + 0 + 1 + 2 + 3 + + diff --git a/sample/src/main/res/values/bools.xml b/sample/src/main/res/values/bools.xml new file mode 100644 index 0000000..a48a910 --- /dev/null +++ b/sample/src/main/res/values/bools.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml new file mode 100644 index 0000000..edfaf55 --- /dev/null +++ b/sample/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + + #FFE12828 + #c32727 + #FF0099CC + + diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml new file mode 100644 index 0000000..47c8224 --- /dev/null +++ b/sample/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/sample/src/main/res/values/integers.xml b/sample/src/main/res/values/integers.xml new file mode 100644 index 0000000..50e618a --- /dev/null +++ b/sample/src/main/res/values/integers.xml @@ -0,0 +1,4 @@ + + + 5 + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000..03d9bfa --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + Psync Sample + Settings + Server + https://www.flipboard.com + POST + diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml new file mode 100644 index 0000000..e0ae3b7 --- /dev/null +++ b/sample/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/sample/src/main/res/xml/prefs_general.xml b/sample/src/main/res/xml/prefs_general.xml new file mode 100644 index 0000000..d60c055 --- /dev/null +++ b/sample/src/main/res/xml/prefs_general.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c06620f --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':psync', ':sample' \ No newline at end of file diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..1fc3c01 --- /dev/null +++ b/version.properties @@ -0,0 +1,4 @@ +#Tue, 01 Sep 2015 03:55:16 -0700 +major=1 +minor=1 +patch=5