diff --git a/.gitignore b/.gitignore index 63e899b..09b993d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,8 @@ -# Built application files -*.apk -*.ap_ - -# Files for the Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin/ -gen/ - -# Gradle files -.gradle/ -build/ -/*/build/ - -# Local configuration file (sdk path, etc) -local.properties - -# Log Files -*.log - -.gradle -.idea -.DS_Store -/captures *.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/build.gradle b/build.gradle index 0a7ccae..7accccb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,28 +1,36 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion compileSdk - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 +buildscript { + ext.kotlin_version = '1.2.60' + repositories { + google() + jcenter() } - - defaultConfig { - minSdkVersion 16 + dependencies { + classpath 'com.android.tools.build:gradle:3.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } -dependencies { - api project(':libraries:core') - - compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - compileOnly "com.android.support:appcompat-v7:$supportLibraryVersion" - compileOnly "com.android.support:design:$supportLibraryVersion" - compileOnly "com.android.support:recyclerview-v7:$supportLibraryVersion" - - compileOnly "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" - compileOnly "io.reactivex.rxjava2:rxjava:$rxJavaVersion" +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext { + versions = [ + compileSdk : 27, + minSdk : 19, + supportLibrary: '27.1.1', + navigation : '1.0.0-alpha04', + lifecycle : '1.1.1', + dagger : '2.16', + retrofit : '2.4.0', + rxJava : '2.1.17', + rxAndroid : '2.0.2' + ] } diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e30af81 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# 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. +org.gradle.jvmargs=-Xmx1536m +# 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 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7a3265e 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..190eeaa --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Aug 05 23:37:20 MSK 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# 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 +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +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" -a "$nonstop" = "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"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # 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 + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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 + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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= + +@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 Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_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=%* + +: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/logging/.gitignore b/logging/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/logging/.gitignore @@ -0,0 +1 @@ +/build diff --git a/logging/build.gradle b/logging/build.gradle new file mode 100644 index 0000000..217c17b --- /dev/null +++ b/logging/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion 16 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "com.android.support:support-annotations:$versions.supportLibrary" +} diff --git a/logging/src/main/AndroidManifest.xml b/logging/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8ce26b3 --- /dev/null +++ b/logging/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java b/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java new file mode 100644 index 0000000..0c05b84 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * Simple {@link LogProcessor} implementation which is logging messages to console (logcat). + */ +public class ConsoleLogProcessor extends LogProcessor { + + private static final int MAX_LOG_LENGTH = 4000; + + public ConsoleLogProcessor(@NonNull final LcLevel lclevel) { + super(lclevel); + } + + @NonNull + private String normalize(@NonNull final String message) { + return message.replace("\r\n", "\n").replace("\0", ""); + } + + @Override + @SuppressWarnings({"WrongConstant", "LogConditional"}) + //WrongConstant, LogConditional: level.getPriority() is not wrong constant! + public void processLogMessage(@NonNull final LcGroup group, @NonNull final LcLevel level, + @NonNull final String tag, @NonNull final String message, @Nullable final Throwable throwable) { + final String messageToLog = normalize(message + (throwable != null ? '\n' + Log.getStackTraceString(throwable) : "")); + final int length = messageToLog.length(); + for (int i = 0; i < length; i++) { + int newline = messageToLog.indexOf('\n', i); + newline = newline != -1 ? newline : length; + do { + final int end = Math.min(newline, i + MAX_LOG_LENGTH); + Log.println(level.getPriority(), tag, messageToLog.substring(i, end)); + i = end; + } + while (i < newline); + } + + } + +} \ No newline at end of file diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java b/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java new file mode 100644 index 0000000..3d46668 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * General logging utility of RoboSwag library. + * You can initialize {@link LogProcessor} to intercept log messages and make decision how to show them. + * Also you can specify assertions behavior to manually make application more stable in production but intercept illegal states in some + * third-party tool to fix them later but not crash in production. + */ +@SuppressWarnings({"checkstyle:methodname", "PMD.ShortMethodName", "PMD.ShortClassName"}) +//MethodNameCheck,ShortMethodName: log methods better be 1-symbol +public final class Lc { + + public static final LcGroup GENERAL_LC_GROUP = new LcGroup("GENERAL"); + + public static final int STACK_TRACE_CODE_DEPTH; + + private static boolean crashOnAssertions = true; + @NonNull + private static LogProcessor logProcessor = new ConsoleLogProcessor(LcLevel.ERROR); + + static { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + int stackDepth; + for (stackDepth = 0; stackDepth < stackTrace.length; stackDepth++) { + if (stackTrace[stackDepth].getClassName().equals(Lc.class.getName())) { + break; + } + } + STACK_TRACE_CODE_DEPTH = stackDepth + 1; + } + + /** + * Flag to crash application or pass it to {@link LogProcessor#processLogMessage(LcGroup, LcLevel, String, String, Throwable)} + * on specific {@link LcGroup#assertion(Throwable)} points of code. + * + * @return True if application should crash on assertion. + */ + public static boolean isCrashOnAssertions() { + return crashOnAssertions; + } + + /** + * Returns {@link LogProcessor} object to intercept incoming log messages (by default it returns {@link ConsoleLogProcessor}). + * + * @return Specific {@link LogProcessor}. + */ + @NonNull + public static LogProcessor getLogProcessor() { + return logProcessor; + } + + /** + * Initialize general logging behavior. + * + * @param logProcessor {@link LogProcessor} to intercept all log messages; + * @param crashOnAssertions Flag to crash application + * or pass it to {@link LogProcessor#processLogMessage(LcGroup, LcLevel, String, String, Throwable)} + * on specific {@link LcGroup#assertion(Throwable)} points of code. + */ + public static void initialize(@NonNull final LogProcessor logProcessor, final boolean crashOnAssertions) { + Lc.crashOnAssertions = crashOnAssertions; + Lc.logProcessor = logProcessor; + } + + /** + * Logs debug message via {@link #GENERAL_LC_GROUP}. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void d(@NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.d(message, args); + } + + /** + * Logs debug message via {@link #GENERAL_LC_GROUP}. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void d(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.d(throwable, message, args); + } + + /** + * Logs info message via {@link #GENERAL_LC_GROUP}. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void i(@NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.i(message, args); + } + + /** + * Logs info message via {@link #GENERAL_LC_GROUP}. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void i(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.i(throwable, message, args); + } + + /** + * Logs warning message via {@link #GENERAL_LC_GROUP}. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void w(@NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.w(message, args); + } + + /** + * Logs warning message via {@link #GENERAL_LC_GROUP}. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void w(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.w(throwable, message, args); + } + + /** + * Logs error message via {@link #GENERAL_LC_GROUP}. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void e(@NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.e(message, args); + } + + /** + * Logs error message via {@link #GENERAL_LC_GROUP}. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void e(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.e(throwable, message, args); + } + + /** + * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app. + * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}. + * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}. + * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build. + * + * @param message Message that is describing assertion. + */ + public static void assertion(@NonNull final String message) { + GENERAL_LC_GROUP.assertion(message); + } + + /** + * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app. + * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}. + * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}. + * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build. + * + * @param throwable Exception that is describing assertion. + */ + public static void assertion(@NonNull final Throwable throwable) { + GENERAL_LC_GROUP.assertion(throwable); + } + + /** + * Throws assertion on main thread (to avoid Rx exceptions e.g.) and cuts top causes by type of exception class. + * + * @param assertion Source throwable; + * @param exceptionsClassesToCut Classes which will be cut from top of causes stack of source throwable. + */ + @SafeVarargs + public static void cutAssertion(@NonNull final Throwable assertion, @NonNull final Class... exceptionsClassesToCut) { + new Handler(Looper.getMainLooper()).post(() -> { + final List processedExceptions = new ArrayList<>(); + Throwable result = assertion; + boolean exceptionAssignableFromIgnores; + do { + exceptionAssignableFromIgnores = false; + processedExceptions.add(result); + for (final Class exceptionClass : exceptionsClassesToCut) { + if (result.getClass().isAssignableFrom(exceptionClass)) { + exceptionAssignableFromIgnores = true; + result = result.getCause(); + break; + } + } + } + while (exceptionAssignableFromIgnores && result != null && !processedExceptions.contains(result)); + Lc.assertion(result != null ? result : assertion); + }); + } + + /** + * Returns line of code from where this method called. + * + * @param caller Object who is calling for code point; + * @return String represents code point. + */ + @NonNull + public static String getCodePoint(@Nullable final Object caller) { + return getCodePoint(caller, 1); + } + + /** + * Returns line of code from where this method called. + * + * @param caller Object who is calling for code point; + * @param stackShift caller Shift of stack (e.g. 2 means two elements deeper); + * @return String represents code point. + */ + @NonNull + public static String getCodePoint(@Nullable final Object caller, final int stackShift) { + final StackTraceElement traceElement = Thread.currentThread().getStackTrace()[STACK_TRACE_CODE_DEPTH + stackShift]; + return traceElement.getMethodName() + '(' + traceElement.getFileName() + ':' + traceElement.getLineNumber() + ')' + + (caller != null ? " of object " + caller.getClass().getSimpleName() + '(' + Integer.toHexString(caller.hashCode()) + ')' : ""); + } + + /** + * Prints stacktrace in log with specified tag. + * + * @param tag Tag to be shown in logs. + */ + + @SuppressLint("LogConditional") + public static void printStackTrace(@NonNull final String tag) { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, TextUtils.join("\n", Arrays.copyOfRange(stackTrace, STACK_TRACE_CODE_DEPTH, stackTrace.length))); + } + } + + private Lc() { + } + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java new file mode 100644 index 0000000..3ccf1fa --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.text.SimpleDateFormat; +import java.util.Locale; + +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; +import ru.touchin.roboswag.core.utils.ThreadLocalValue; + +/** + * Created by Gavriil Sitnikov on 14/05/2016. + * Group of log messages with specific tag prefix (name of group). + * It could be used in specific {@link LogProcessor} to filter messages by group. + */ +@SuppressWarnings({"checkstyle:methodname", "PMD.ShortMethodName"}) +//MethodNameCheck,ShortMethodName: log methods better be 1-symbol +public class LcGroup { + + /** + * Logging group to log UI metrics (like inflation or layout time etc.). + */ + public static final LcGroup UI_METRICS = new LcGroup("UI_METRICS"); + /** + * Logging group to log UI lifecycle (onCreate, onStart, onResume etc.). + */ + public static final LcGroup UI_LIFECYCLE = new LcGroup("UI_LIFECYCLE"); + + private static final ThreadLocalValue DATE_TIME_FORMATTER + = new ThreadLocalValue<>(() -> new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())); + + @NonNull + private final String name; + private boolean disabled; + + public LcGroup(@NonNull final String name) { + this.name = name; + } + + /** + * Disables logging of this group. + */ + public void disable() { + disabled = true; + } + + /** + * Enables logging of this group. + */ + public void enable() { + disabled = false; + } + + @NonNull + private String createLogTag() { + final StackTraceElement trace = Thread.currentThread().getStackTrace()[Lc.STACK_TRACE_CODE_DEPTH + 3]; + return trace.getFileName() + ':' + trace.getLineNumber(); + } + + @SuppressWarnings("PMD.AvoidCatchingThrowable") + //AvoidCatchingThrowable: it is needed to safety format message + @Nullable + private String createFormattedMessage(@Nullable final String message, @NonNull final Object... args) { + try { + if (args.length > 0 && message == null) { + throw new ShouldNotHappenException("Args are not empty but format message is null"); + } + return message != null ? (args.length > 0 ? String.format(message, args) : message) : null; + } catch (final Throwable formattingException) { + Lc.assertion(formattingException); + return null; + } + } + + @NonNull + private String createLogMessage(@Nullable final String formattedMessage) { + return DATE_TIME_FORMATTER.get().format(System.currentTimeMillis()) + + ' ' + Thread.currentThread().getName() + + ' ' + name + + (formattedMessage != null ? (' ' + formattedMessage) : ""); + } + + private void logMessage(@NonNull final LcLevel logLevel, @Nullable final String message, + @Nullable final Throwable throwable, @NonNull final Object... args) { + if (disabled || logLevel.lessThan(Lc.getLogProcessor().getMinLogLevel())) { + return; + } + + if (throwable == null && args.length > 0 && args[0] instanceof Throwable) { + Lc.w("Maybe you've misplaced exception with first format arg? format: %s; arg: %s", message, args[0]); + } + + final String formattedMessage = createFormattedMessage(message, args); + if (logLevel == LcLevel.ASSERT && Lc.isCrashOnAssertions()) { + throw createAssertion(formattedMessage, throwable); + } + + Lc.getLogProcessor().processLogMessage(this, logLevel, createLogTag(), createLogMessage(formattedMessage), throwable); + } + + @NonNull + private ShouldNotHappenException createAssertion(@Nullable final String message, @Nullable final Throwable exception) { + return exception != null + ? (message != null ? new ShouldNotHappenException(message, exception) + : (exception instanceof ShouldNotHappenException ? (ShouldNotHappenException) exception : new ShouldNotHappenException(exception))) + : (message != null ? new ShouldNotHappenException(message) : new ShouldNotHappenException()); + } + + /** + * Logs debug message. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void d(@NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.DEBUG, message, null, args); + } + + /** + * Logs debug message. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void d(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.DEBUG, message, throwable, args); + } + + /** + * Logs info message. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void i(@NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.INFO, message, null, args); + } + + /** + * Logs info message. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void i(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.INFO, message, throwable, args); + } + + /** + * Logs warning message. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void w(@NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.WARN, message, null, args); + } + + /** + * Logs warning message. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void w(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.WARN, message, throwable, args); + } + + /** + * Logs error message. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void e(@NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.ERROR, message, null, args); + } + + /** + * Logs error message. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void e(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.ERROR, message, throwable, args); + } + + /** + * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app. + * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}. + * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}. + * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build. + * + * @param message Message that is describing assertion. + */ + public void assertion(@NonNull final String message) { + logMessage(LcLevel.ASSERT, "Assertion appears at %s with message: %s", null, Lc.getCodePoint(null, 2), message); + } + + /** + * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app. + * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}. + * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}. + * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build. + * + * @param throwable Exception that is describing assertion. + */ + public void assertion(@NonNull final Throwable throwable) { + logMessage(LcLevel.ASSERT, "Assertion appears at %s", throwable, Lc.getCodePoint(null, 2)); + } + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java new file mode 100644 index 0000000..9dbd5e5 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.support.annotation.NonNull; +import android.util.Log; + +/** + * Created by Gavriil Sitnikov on 14/05/2016. + * Level of log message. + */ +public enum LcLevel { + + VERBOSE(Log.VERBOSE), + DEBUG(Log.DEBUG), + INFO(Log.INFO), + WARN(Log.WARN), + ERROR(Log.ERROR), + ASSERT(Log.ASSERT); + + private final int priority; + + LcLevel(final int priority) { + this.priority = priority; + } + + /** + * Standard {@link Log} integer value of level represents priority of message. + * + * @return Integer level. + */ + public int getPriority() { + return priority; + } + + /** + * Compares priorities of LcLevels and returns if current is less than another. + * + * @param logLevel {@link LcLevel} to compare priority with; + * @return True if current level priority less than level passed as parameter. + */ + public boolean lessThan(@NonNull final LcLevel logLevel) { + return this.priority < logLevel.priority; + } + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java new file mode 100644 index 0000000..6cb34a9 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * Abstract object to intercept log messages coming from {@link LcGroup} and {@link Lc} log methods. + */ +public abstract class LogProcessor { + + @NonNull + private final LcLevel minLogLevel; + + public LogProcessor(@NonNull final LcLevel minLogLevel) { + this.minLogLevel = minLogLevel; + } + + /** + * Minimum logging level. + * Any messages with lower priority won't be passed into {@link #processLogMessage(LcGroup, LcLevel, String, String, Throwable)}. + * + * @return Minimum log level represented by {@link LcLevel} object. + */ + @NonNull + public LcLevel getMinLogLevel() { + return minLogLevel; + } + + /** + * Core method to process any incoming log messages from {@link LcGroup} and {@link Lc} with level higher or equals {@link #getMinLogLevel()}. + * + * @param group {@link LcGroup} where log message came from; + * @param level {@link LcLevel} level (priority) of message; + * @param tag String mark of message; + * @param message Message to log; + * @param throwable Exception to log. + */ + public abstract void processLogMessage(@NonNull final LcGroup group, @NonNull final LcLevel level, + @NonNull final String tag, @NonNull final String message, @Nullable final Throwable throwable); + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java b/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java new file mode 100644 index 0000000..b3f11f0 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.NonNull; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * Exception that should be threw when some unexpected code reached. + * E.g. if some value null but it is not legal or in default case in switch if all specific cases should be processed. + */ +public class ShouldNotHappenException extends RuntimeException { + + private static final long serialVersionUID = 0; + + public ShouldNotHappenException() { + super(); + } + + public ShouldNotHappenException(@NonNull final String detailMessage) { + super(detailMessage); + } + + public ShouldNotHappenException(@NonNull final String detailMessage, @NonNull final Throwable throwable) { + super(detailMessage, throwable); + } + + public ShouldNotHappenException(@NonNull final Throwable throwable) { + super(throwable); + } + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java b/logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java new file mode 100644 index 0000000..62cf778 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.NonNull; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * Thread local value with specified creator of value per thread. + */ +public class ThreadLocalValue extends ThreadLocal { + + @NonNull + private final Fabric fabric; + + public ThreadLocalValue(@NonNull final Fabric fabric) { + super(); + this.fabric = fabric; + } + + @NonNull + @Override + protected T initialValue() { + return fabric.create(); + } + + /** + * Fabric of thread-local objects. + * + * @param Type of objects. + */ + public interface Fabric { + + /** + * Creates object. + * + * @return new instance of object. + */ + @NonNull + T create(); + + } + +} diff --git a/navigation/.gitignore b/navigation/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/navigation/.gitignore @@ -0,0 +1 @@ +/build diff --git a/navigation/build.gradle b/navigation/build.gradle new file mode 100644 index 0000000..26d4258 --- /dev/null +++ b/navigation/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion 16 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":utils") + api project(":logging") + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "com.android.support:appcompat-v7:$versions.supportLibrary" +} diff --git a/navigation/src/main/AndroidManifest.xml b/navigation/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bd2d3ee --- /dev/null +++ b/navigation/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java similarity index 99% rename from src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java index 2dfb580..8d68057 100644 --- a/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java @@ -36,4 +36,4 @@ public interface OnFragmentStartedListener { */ void onFragmentStarted(@NonNull Fragment fragment); -} \ No newline at end of file +} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java similarity index 83% rename from src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java index 7a3f0b7..ee39486 100644 --- a/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java @@ -29,8 +29,8 @@ import android.view.MenuItem; import java.util.Set; -import ru.touchin.roboswag.components.utils.UiUtils; import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; /** * Created by Gavriil Sitnikov on 08/03/2016. @@ -44,54 +44,54 @@ public abstract class BaseActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); } @Override protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { super.onActivityResult(requestCode, resultCode, data); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this) + " requestCode: " + requestCode + "; resultCode: " + resultCode); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this) + " requestCode: " + requestCode + "; resultCode: " + resultCode); } @Override protected void onStart() { super.onStart(); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); } @Override protected void onResume() { super.onResume(); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); } @Override protected void onPause() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); super.onPause(); } @Override protected void onSaveInstanceState(@NonNull final Bundle stateToSave) { super.onSaveInstanceState(stateToSave); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); } @Override public void onLowMemory() { super.onLowMemory(); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); } @Override protected void onStop() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); super.onStop(); } @Override protected void onDestroy() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); super.onDestroy(); } diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java similarity index 95% rename from src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java index 74f290d..065f06b 100644 --- a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java @@ -30,7 +30,6 @@ import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; -import android.support.v4.view.ViewCompat; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -42,10 +41,9 @@ import android.widget.FrameLayout; import java.lang.reflect.Constructor; -import ru.touchin.roboswag.components.R; import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController; -import ru.touchin.roboswag.components.utils.UiUtils; import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; import ru.touchin.roboswag.core.utils.ShouldNotHappenException; /** @@ -182,7 +180,7 @@ public class ViewControllerFragment acceptableUiCalculationTime) { - UiUtils.UI_METRICS_LC_GROUP.w("Creation of %s took too much: %dms", viewControllerClass, creationPeriod); + LcGroup.UI_METRICS.w("Creation of %s took too much: %dms", viewControllerClass, creationPeriod); } } } @@ -212,11 +210,6 @@ public class ViewControllerFragment 0) { final long layoutTime = SystemClock.uptimeMillis() - lastMeasureTime; if (layoutTime > acceptableUiCalculationTime) { - UiUtils.UI_METRICS_LC_GROUP.w("Measure and layout of %s took too much: %dms", tagName, layoutTime); + LcGroup.UI_METRICS.w("Measure and layout of %s took too much: %dms", tagName, layoutTime); } lastMeasureTime = 0; } diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java similarity index 99% rename from src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java index 5cd873f..bca1f28 100644 --- a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java @@ -30,9 +30,9 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import io.reactivex.functions.BiConsumer; import ru.touchin.roboswag.components.navigation.OnFragmentStartedListener; import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.utils.BiConsumer; /** * Created by Gavriil Sitnikov on 21/10/2015. diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java similarity index 93% rename from src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java index 363e628..8e4c7d0 100644 --- a/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java @@ -46,18 +46,19 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; - import android.view.animation.Animation; + import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment; import ru.touchin.roboswag.components.utils.UiUtils; import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; /** * Created by Gavriil Sitnikov on 21/10/2015. * Class to control view of specific fragment, activity and application by logic bridge. * * @param Type of activity where such {@link ViewController} could be; - * @param Type of state; + * @param Type of state; */ public class ViewController implements LifecycleOwner { @@ -221,7 +222,7 @@ public class ViewController *

Starting in {@link android.os.Build.VERSION_CODES#M}, the returned * color state list will be styled for the specified Context's theme. * @@ -275,7 +276,7 @@ public class ViewController + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/java/ru/touchin/roboswag/components/MainActivity.kt b/sample/src/main/java/ru/touchin/roboswag/components/MainActivity.kt new file mode 100644 index 0000000..ebf42b3 --- /dev/null +++ b/sample/src/main/java/ru/touchin/roboswag/components/MainActivity.kt @@ -0,0 +1,12 @@ +package ru.touchin.roboswag.components + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..3e810c0 --- /dev/null +++ b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..5713f34 --- /dev/null +++ b/sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..7539a01 --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..f4bc9d9 --- /dev/null +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..f4bc9d9 --- /dev/null +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file 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..a2f5908 Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1b52399 Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.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..ff10afd Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..115a4c7 Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.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..dcd3cd8 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..459ca60 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.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..8ca12fe Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e19b41 Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b824ebd Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4c19a13 Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/sample/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000..f6470ae --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Components + diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml new file mode 100644 index 0000000..5885930 --- /dev/null +++ b/sample/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9e1f446 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':sample', ':utils', ':logging', ':navigation', ':storable' diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml deleted file mode 100644 index c76023c..0000000 --- a/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java deleted file mode 100644 index 25be0c9..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * 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. - * - */ - -package ru.touchin.roboswag.components.adapters; - -import android.support.annotation.NonNull; -import android.support.v4.view.ViewCompat; -import android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; - -import java.util.List; - -/** - * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. - * Default {@link #getItemViewType} is generating on construction of object. - * - * @param Type of {@link RecyclerView.ViewHolder} of delegate. - */ -public abstract class AdapterDelegate { - - private final int defaultItemViewType = ViewCompat.generateViewId(); - - /** - * Unique ID of AdapterDelegate. - * - * @return Unique ID. - */ - public int getItemViewType() { - return defaultItemViewType; - } - - /** - * Returns if object is processable by this delegate. - * - * @param items Items to check; - * @param adapterPosition Position of item in adapter; - * @param collectionPosition Position of item in collection; - * @return True if item is processable by this delegate. - */ - public abstract boolean isForViewType(@NonNull final List items, final int adapterPosition, final int collectionPosition); - - /** - * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. - * - * @param items Items in adapter; - * @param adapterPosition Position of item in adapter; - * @param collectionPosition Position of item in collection; - * @return Unique item ID. - */ - public long getItemId(@NonNull final List items, final int adapterPosition, final int collectionPosition) { - return 0; - } - - /** - * Creates ViewHolder to bind item to it later. - * - * @param parent Container of ViewHolder's view. - * @return New ViewHolder. - */ - @NonNull - public abstract TViewHolder onCreateViewHolder(@NonNull final ViewGroup parent); - - /** - * Binds item to created by this object ViewHolder. - * - * @param holder ViewHolder to bind item to; - * @param items Items in adapter; - * @param adapterPosition Position of item in adapter; - * @param collectionPosition Position of item in collection that contains item; - * @param payloads Payloads; - */ - public abstract void onBindViewHolder( - @NonNull final RecyclerView.ViewHolder holder, - @NonNull final List items, - final int adapterPosition, - final int collectionPosition, - @NonNull final List payloads - ); - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt b/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt deleted file mode 100644 index e3ba14f..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt +++ /dev/null @@ -1,52 +0,0 @@ -package ru.touchin.roboswag.components.adapters - -import android.support.v7.widget.RecyclerView -import android.util.SparseArray -import android.view.ViewGroup - -/** - * Manager for delegation callbacks from [RecyclerView.Adapter] to delegates. - */ -class DelegatesManager { - - private val delegates = SparseArray>() - - fun getItemViewType(items: List<*>, adapterPosition: Int, collectionPosition: Int): Int { - for (index in 0 until delegates.size()) { - val delegate = delegates.valueAt(index) - if (delegate.isForViewType(items, adapterPosition, collectionPosition)) { - return delegate.itemViewType - } - } - throw IllegalStateException("Delegate not found for adapterPosition: $adapterPosition") - } - - fun getItemId(items: List<*>, adapterPosition: Int, collectionPosition: Int): Long { - val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition)) - return delegate.getItemId(items, adapterPosition, collectionPosition) - } - - fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = getDelegate(viewType).onCreateViewHolder(parent) - - fun onBindViewHolder(holder: RecyclerView.ViewHolder, items: List<*>, adapterPosition: Int, collectionPosition: Int, payloads: List) { - val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition)) - delegate.onBindViewHolder(holder, items, adapterPosition, collectionPosition, payloads) - } - - /** - * Adds [PositionAdapterDelegate] to adapter. - * - * @param delegate Delegate to add. - */ - fun addDelegate(delegate: AdapterDelegate<*>) = delegates.put(delegate.itemViewType, delegate) - - /** - * Removes [AdapterDelegate] from adapter. - * - * @param delegate Delegate to remove. - */ - fun removeDelegate(delegate: AdapterDelegate<*>) = delegates.remove(delegate.itemViewType) - - private fun getDelegate(viewType: Int) = delegates[viewType] ?: throw IllegalStateException("No AdapterDelegate added for view type: $viewType") - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt b/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt deleted file mode 100644 index 094b298..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt +++ /dev/null @@ -1,89 +0,0 @@ -package ru.touchin.roboswag.components.adapters - -import android.support.v7.recyclerview.extensions.AsyncDifferConfig -import android.support.v7.recyclerview.extensions.AsyncListDiffer -import android.support.v7.util.DiffUtil -import android.support.v7.widget.RecyclerView -import android.view.ViewGroup -import ru.touchin.roboswag.components.extensions.setOnRippleClickListener - -/** - * Base adapter with delegation and diff computing on background thread. - */ -open class DelegationListAdapter(config: AsyncDifferConfig) : RecyclerView.Adapter() { - - constructor(diffCallback: DiffUtil.ItemCallback) : this(AsyncDifferConfig.Builder(diffCallback).build()) - - var itemClickListener: ((TItem, RecyclerView.ViewHolder) -> Unit)? = null - - private val delegatesManager = DelegatesManager() - private var differ = AsyncListDiffer(OffsetAdapterUpdateCallback(this, ::getHeadersCount), config) - - open fun getHeadersCount() = 0 - - open fun getFootersCount() = 0 - - override fun getItemCount() = getHeadersCount() + getList().size + getFootersCount() - - override fun getItemViewType(position: Int) = delegatesManager.getItemViewType(getList(), position, getCollectionPosition(position)) - - override fun getItemId(position: Int) = delegatesManager.getItemId(getList(), position, getCollectionPosition(position)) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = delegatesManager.onCreateViewHolder(parent, viewType) - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { - val collectionPosition = getCollectionPosition(position) - if (collectionPosition in 0 until getList().size) { - if (itemClickListener != null) { - holder.itemView.setOnRippleClickListener { - itemClickListener?.invoke(getList()[getCollectionPosition(holder.adapterPosition)], holder) - } - } else { - holder.itemView.setOnClickListener(null) - } - } - delegatesManager.onBindViewHolder(holder, getList(), position, collectionPosition, payloads) - } - - final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = Unit - - /** - * Adds [AdapterDelegate] to adapter. - * - * @param delegate Delegate to add. - */ - fun addDelegate(delegate: AdapterDelegate<*>) = delegatesManager.addDelegate(delegate) - - /** - * Removes [AdapterDelegate] from adapter. - * - * @param delegate Delegate to remove. - */ - fun removeDelegate(delegate: AdapterDelegate<*>) = delegatesManager.removeDelegate(delegate) - - /** - * Submits a new list to be diffed, and displayed. - * - * If a list is already being displayed, a diff will be computed on a background thread, which - * will dispatch Adapter.notifyItem events on the main thread. - * - * @param list The new list to be displayed. - */ - fun submitList(list: List) = differ.submitList(list) - - /** - * Get the current List - any diffing to present this list has already been computed and - * dispatched via the ListUpdateCallback. - *

- * If a null List, or no List has been submitted, an empty list will be returned. - *

- * The returned list may not be mutated - mutations to content must be done through - * {@link #submitList(List)}. - * - * @return current List. - */ - fun getList(): List = differ.currentList - - fun getCollectionPosition(adapterPosition: Int) = adapterPosition - getHeadersCount() - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java deleted file mode 100644 index 324bcf4..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java +++ /dev/null @@ -1,85 +0,0 @@ -package ru.touchin.roboswag.components.adapters; - -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; - -import java.util.List; - -/** - * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. - * Such delegates are creating and binding ViewHolders for specific items. - * Default {@link #getItemViewType} is generating on construction of object. - * - * @param Type of {@link RecyclerView.ViewHolder} of delegate; - * @param Type of items to bind to {@link RecyclerView.ViewHolder}s. - */ -public abstract class ItemAdapterDelegate extends AdapterDelegate { - - @Override - public boolean isForViewType(@NonNull final List items, final int adapterPosition, final int collectionPosition) { - return collectionPosition >= 0 - && collectionPosition < items.size() - && isForViewType(items.get(collectionPosition), adapterPosition, collectionPosition); - } - - /** - * Returns if object is processable by this delegate. - * This item will be casted to {@link TItem} and passes to {@link #onBindViewHolder(TViewHolder, TItem, int, int, List)}. - * - * @param item Item to check; - * @param adapterPosition Position of item in adapter; - * @param collectionPosition Position of item in collection that contains item; - * @return True if item is processable by this delegate. - */ - public boolean isForViewType(@NonNull final Object item, final int adapterPosition, final int collectionPosition) { - return true; - } - - @Override - public long getItemId(@NonNull final List items, final int adapterPosition, final int collectionPosition) { - //noinspection unchecked - return getItemId((TItem) items.get(collectionPosition), adapterPosition, collectionPosition); - } - - /** - * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. - * - * @param item Item in adapter; - * @param adapterPosition Position of item in adapter; - * @param collectionPosition Position of item in collection that contains item; - * @return Unique item ID. - */ - public long getItemId(@NonNull final TItem item, final int adapterPosition, final int collectionPosition) { - return 0; - } - - @Override - public void onBindViewHolder( - @NonNull final RecyclerView.ViewHolder holder, - @NonNull final List items, - final int adapterPosition, - final int collectionPosition, - @NonNull final List payloads - ) { - //noinspection unchecked - onBindViewHolder((TViewHolder) holder, (TItem) items.get(collectionPosition), adapterPosition, collectionPosition, payloads); - } - - /** - * Binds item with payloads to created by this object ViewHolder. - * - * @param holder ViewHolder to bind item to; - * @param item Item in adapter; - * @param adapterPosition Position of item in adapter; - * @param collectionPosition Position of item in collection that contains item; - * @param payloads Payloads; - */ - public abstract void onBindViewHolder( - @NonNull final TViewHolder holder, - @NonNull final TItem item, - final int adapterPosition, - final int collectionPosition, - @NonNull final List payloads - ); - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt b/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt deleted file mode 100644 index 9715eb2..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt +++ /dev/null @@ -1,24 +0,0 @@ -package ru.touchin.roboswag.components.adapters - -import android.support.v7.util.ListUpdateCallback -import android.support.v7.widget.RecyclerView - -class OffsetAdapterUpdateCallback(private val adapter: RecyclerView.Adapter<*>, private val offsetProvider: () -> Int) : ListUpdateCallback { - - override fun onInserted(position: Int, count: Int) { - adapter.notifyItemRangeInserted(position + offsetProvider(), count) - } - - override fun onRemoved(position: Int, count: Int) { - adapter.notifyItemRangeRemoved(position + offsetProvider(), count) - } - - override fun onMoved(fromPosition: Int, toPosition: Int) { - adapter.notifyItemMoved(fromPosition + offsetProvider(), toPosition + offsetProvider()) - } - - override fun onChanged(position: Int, count: Int, payload: Any?) { - adapter.notifyItemRangeChanged(position + offsetProvider(), count, payload) - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java deleted file mode 100644 index ada8888..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java +++ /dev/null @@ -1,68 +0,0 @@ -package ru.touchin.roboswag.components.adapters; - -import android.support.annotation.NonNull; -import android.support.v7.widget.RecyclerView; - -import java.util.List; - -/** - * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. - * Such delegates are creating and binding ViewHolders by position in adapter. - * Default {@link #getItemViewType} is generating on construction of object. - * - * @param Type of {@link RecyclerView.ViewHolder} of delegate. - */ -public abstract class PositionAdapterDelegate extends AdapterDelegate { - - @Override - public boolean isForViewType(@NonNull final List items, final int adapterPosition, final int collectionPosition) { - return isForViewType(adapterPosition); - } - - /** - * Returns if object is processable by this delegate. - * - * @param adapterPosition Position of item in adapter; - * @return True if item is processable by this delegate. - */ - public abstract boolean isForViewType(final int adapterPosition); - - @Override - public long getItemId(@NonNull final List objects, final int adapterPosition, final int itemsOffset) { - return getItemId(adapterPosition); - } - - /** - * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. - * - * @param adapterPosition Position of item in adapter; - * @return Unique item ID. - */ - public long getItemId(final int adapterPosition) { - return 0; - } - - @Override - public void onBindViewHolder( - @NonNull final RecyclerView.ViewHolder holder, - @NonNull final List items, - final int adapterPosition, - final int collectionPosition, - @NonNull final List payloads - ) { - //noinspection unchecked - onBindViewHolder((TViewHolder) holder, adapterPosition, payloads); - } - - /** - * Binds position with payloads to ViewHolder. - * - * @param holder ViewHolder to bind position to; - * @param adapterPosition Position of item in adapter; - * @param payloads Payloads. - */ - public void onBindViewHolder(@NonNull final TViewHolder holder, final int adapterPosition, @NonNull final List payloads) { - //do nothing by default - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt b/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt deleted file mode 100644 index 050bd6b..0000000 --- a/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt +++ /dev/null @@ -1,24 +0,0 @@ -package ru.touchin.roboswag.components.extensions - -import kotlin.properties.Delegates -import kotlin.properties.ObservableProperty -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -/** - * Simple observable delegate only for notification of new value. - */ -inline fun Delegates.observable( - initialValue: T, - crossinline onChange: (newValue: T) -> Unit -): ReadWriteProperty = object : ObservableProperty(initialValue) { - override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(newValue) -} - -inline fun Delegates.distinctUntilChanged( - initialValue: T, - crossinline onChange: (newValue: T) -> Unit -): ReadWriteProperty = object : ObservableProperty(initialValue) { - override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = - if (newValue != null && oldValue != newValue) onChange(newValue) else Unit -} diff --git a/src/main/java/ru/touchin/roboswag/components/extensions/View.kt b/src/main/java/ru/touchin/roboswag/components/extensions/View.kt deleted file mode 100644 index 9f4ddca..0000000 --- a/src/main/java/ru/touchin/roboswag/components/extensions/View.kt +++ /dev/null @@ -1,20 +0,0 @@ -package ru.touchin.roboswag.components.extensions - -import android.os.Build -import android.view.View - -private const val RIPPLE_EFFECT_DELAY = 150L - -/** - * Sets click listener to view. On click it will call something after delay. - * - * @param delay Delay after which click listener will be called; - * @param listener Click listener. - */ -fun View.setOnRippleClickListener(delay: Long = RIPPLE_EFFECT_DELAY, listener: (View) -> Unit) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - setOnClickListener { view -> postDelayed({ if (hasWindowFocus()) listener(view) }, delay) } - } else { - setOnClickListener(listener) - } -} diff --git a/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt b/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt deleted file mode 100644 index db7165f..0000000 --- a/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ru.touchin.roboswag.components.extensions - -import android.content.Context -import android.content.res.ColorStateList -import android.graphics.drawable.Drawable -import android.support.annotation.ColorInt -import android.support.annotation.ColorRes -import android.support.annotation.DrawableRes -import android.support.annotation.IdRes -import android.support.annotation.StringRes -import android.support.v4.content.ContextCompat -import android.support.v7.widget.RecyclerView -import android.view.View - -fun RecyclerView.ViewHolder.findViewById(@IdRes resId: Int): T = itemView.findViewById(resId) - -val RecyclerView.ViewHolder.context: Context - get() = itemView.context - -fun RecyclerView.ViewHolder.getText(@StringRes resId: Int): CharSequence = context.getText(resId) - -fun RecyclerView.ViewHolder.getString(@StringRes resId: Int): String = context.getString(resId) - -@SuppressWarnings("SpreadOperator") // it's OK for small arrays -fun RecyclerView.ViewHolder.getString(@StringRes resId: Int, vararg args: Any): String = context.getString(resId, *args) - -@ColorInt -fun RecyclerView.ViewHolder.getColor(@ColorRes resId: Int): Int = ContextCompat.getColor(context, resId) - -fun RecyclerView.ViewHolder.getColorStateList(@ColorRes resId: Int): ColorStateList? = ContextCompat.getColorStateList(context, resId) - -fun RecyclerView.ViewHolder.getDrawable(@DrawableRes resId: Int): Drawable? = ContextCompat.getDrawable(context, resId) diff --git a/src/main/java/ru/touchin/roboswag/components/utils/audio/HeadsetStateObserver.java b/src/main/java/ru/touchin/roboswag/components/utils/audio/HeadsetStateObserver.java deleted file mode 100644 index 346e52b..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/audio/HeadsetStateObserver.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * 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. - * - */ - -package ru.touchin.roboswag.components.utils.audio; - -import android.bluetooth.BluetoothA2dp; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.media.AudioManager; -import android.support.annotation.NonNull; - -import io.reactivex.Observable; -import io.reactivex.subjects.BehaviorSubject; - -/** - * Created by Gavriil Sitnikov on 02/11/2015. - * Simple observer of wired or wireless (bluetooth A2DP) headsets state (connected or not). - *
You require android.permission.BLUETOOTH and API level >= 11 if want to observe wireless headset state - */ -public final class HeadsetStateObserver { - - @NonNull - private final AudioManager audioManager; - @NonNull - private final Observable connectedObservable; - - public HeadsetStateObserver(@NonNull final Context context) { - audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - connectedObservable = Observable - .fromCallable(() -> new IsConnectedReceiver(audioManager)) - .switchMap(isConnectedReceiver -> Observable.combineLatest(isConnectedReceiver.isWiredConnectedChangedEvent, - isConnectedReceiver.isWirelessConnectedChangedEvent, - (isWiredConnected, isWirelessConnected) -> isWiredConnected || isWirelessConnected) - .distinctUntilChanged() - .doOnSubscribe(disposable -> { - final IntentFilter headsetStateIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); - headsetStateIntentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); - context.registerReceiver(isConnectedReceiver, headsetStateIntentFilter); - }) - .doOnDispose(() -> context.unregisterReceiver(isConnectedReceiver))) - .replay(1) - .refCount(); - } - - /** - * Returns if wired or wireless headset is connected. - * - * @return True if headset is connected. - */ - @SuppressWarnings("deprecation") - public boolean isConnected() { - return audioManager.isWiredHeadsetOn() || audioManager.isBluetoothA2dpOn(); - } - - /** - * Observes connection state of headset. - * - * @return Returns observable which will provide current connection state and any of it's udpdate. - */ - @NonNull - public Observable observeIsConnected() { - return connectedObservable; - } - - private static class IsConnectedReceiver extends BroadcastReceiver { - - @NonNull - private final BehaviorSubject isWiredConnectedChangedEvent; - @NonNull - private final BehaviorSubject isWirelessConnectedChangedEvent; - - @SuppressWarnings("deprecation") - public IsConnectedReceiver(@NonNull final AudioManager audioManager) { - super(); - isWiredConnectedChangedEvent = BehaviorSubject.createDefault(audioManager.isWiredHeadsetOn()); - isWirelessConnectedChangedEvent = BehaviorSubject.createDefault(audioManager.isBluetoothA2dpOn()); - } - - @Override - public void onReceive(@NonNull final Context context, @NonNull final Intent intent) { - if (Intent.ACTION_HEADSET_PLUG.equals(intent.getAction()) && !isInitialStickyBroadcast()) { - isWiredConnectedChangedEvent.onNext(intent.getIntExtra("state", 0) != 0); - } - if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { - final int bluetoothState = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTED); - switch (bluetoothState) { - case BluetoothA2dp.STATE_DISCONNECTED: - isWirelessConnectedChangedEvent.onNext(false); - break; - case BluetoothA2dp.STATE_CONNECTED: - isWirelessConnectedChangedEvent.onNext(true); - break; - default: - break; - } - } - } - - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/utils/audio/VolumeController.java b/src/main/java/ru/touchin/roboswag/components/utils/audio/VolumeController.java deleted file mode 100644 index c0853de..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/audio/VolumeController.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * 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. - * - */ - -package ru.touchin.roboswag.components.utils.audio; - -import android.content.Context; -import android.database.ContentObserver; -import android.media.AudioManager; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import android.support.annotation.NonNull; - -import io.reactivex.Observable; -import io.reactivex.subjects.PublishSubject; -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; - -/** - * Created by Gavriil Sitnikov on 02/11/2015. - * Simple class to control and observe volume of specific stream type (phone call, music etc.). - */ -public final class VolumeController { - - @NonNull - private final AudioManager audioManager; - private final int maxVolume; - @NonNull - private final Observable volumeObservable; - @NonNull - private final PublishSubject selfVolumeChangedEvent = PublishSubject.create(); - - public VolumeController(@NonNull final Context context) { - this(context, AudioManager.STREAM_MUSIC); - } - - public VolumeController(@NonNull final Context context, final int streamType) { - audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - maxVolume = audioManager.getStreamMaxVolume(streamType); - volumeObservable = Observable - .fromCallable(VolumeObserver::new) - .switchMap(volumeObserver -> selfVolumeChangedEvent - .mergeWith(volumeObserver.systemVolumeChangedEvent - .map(event -> getVolume()) - .doOnSubscribe(disposable -> context.getContentResolver() - .registerContentObserver(Settings.System.CONTENT_URI, true, volumeObserver)) - .doOnDispose(() -> context.getContentResolver() - .unregisterContentObserver(volumeObserver))) - .startWith(getVolume())) - .distinctUntilChanged() - .replay(1) - .refCount(); - } - - /** - * Max volume amount to set. - * - * @return max volume. - */ - public int getMaxVolume() { - return maxVolume; - } - - /** - * Sets volume. - * - * @param volume Volume value to set from 0 to {@link #getMaxVolume()}. - */ - public void setVolume(final int volume) { - if (volume < 0 || volume > maxVolume) { - Lc.assertion(new ShouldNotHappenException("Volume: " + volume + " out of bounds [0," + maxVolume + ']')); - return; - } - if (getVolume() != volume) { - audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); - selfVolumeChangedEvent.onNext(volume); - } - } - - /** - * Returns volume. - * - * @return Returns volume value from 0 to {@link #getMaxVolume()}. - */ - public int getVolume() { - return audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); - } - - /** - * Observes current volume. - * - * @return Observable which will provide current volume and then it's updates. - */ - @NonNull - public Observable observeVolume() { - return volumeObservable; - } - - private static class VolumeObserver extends ContentObserver { - - @NonNull - private final PublishSubject systemVolumeChangedEvent = PublishSubject.create(); - - public VolumeObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public void onChange(final boolean selfChange) { - super.onChange(selfChange); - systemVolumeChangedEvent.onNext(null); - } - - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/utils/images/BlurUtils.java b/src/main/java/ru/touchin/roboswag/components/utils/images/BlurUtils.java deleted file mode 100644 index 6f3837d..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/images/BlurUtils.java +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Copyright (C) 2015 Wasabeef - * 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. - */ - -package ru.touchin.roboswag.components.utils.images; - -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.Bitmap; -import android.os.Build; -import android.renderscript.Allocation; -import android.renderscript.Element; -import android.renderscript.RSRuntimeException; -import android.renderscript.RenderScript; -import android.renderscript.ScriptIntrinsicBlur; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -public final class BlurUtils { - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @Nullable - public static Bitmap blurRenderscript(@NonNull final Context context, @NonNull final Bitmap bitmap, final int radius) throws RSRuntimeException { - RenderScript rs = null; - Allocation input = null; - Allocation output = null; - ScriptIntrinsicBlur blur = null; - try { - rs = RenderScript.create(context); - rs.setMessageHandler(new RenderScript.RSMessageHandler()); - input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE, - Allocation.USAGE_SCRIPT); - output = Allocation.createTyped(rs, input.getType()); - blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); - - blur.setInput(input); - blur.setRadius(radius); - blur.forEach(output); - output.copyTo(bitmap); - } finally { - if (rs != null) { - rs.destroy(); - } - if (input != null) { - input.destroy(); - } - if (output != null) { - output.destroy(); - } - if (blur != null) { - blur.destroy(); - } - } - - return bitmap; - } - - @Nullable - @SuppressWarnings({"PMD.ExcessiveMethodLength", "PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", "PMD.StdCyclomaticComplexity", - "PMD.NcssMethodCount", "PMD.NPathComplexity", "checkstyle:MethodLength", "checkstyle:LocalFinalVariableName", - "checkstyle:ArrayTypeStyle", "checkstyle:InnerAssignment", "checkstyle:LocalVariableName"}) - public static Bitmap blurFast(@NonNull final Bitmap sentBitmap, final int radius, final boolean canReuseInBitmap) { - - // Stack Blur v1.0 from - // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html - // - // Java Author: Mario Klingemann - // http://incubator.quasimondo.com - // created Feburary 29, 2004 - // Android port : Yahel Bouaziz - // http://www.kayenko.com - // ported april 5th, 2012 - - // This is a compromise between Gaussian Blur and Box blur - // It creates much better looking blurs than Box Blur, but is - // 7x faster than my Gaussian Blur implementation. - // - // I called it Stack Blur because this describes best how this - // filter works internally: it creates a kind of moving stack - // of colors whilst scanning through the image. Thereby it - // just has to add one new block of color to the right side - // of the stack and remove the leftmost color. The remaining - // colors on the topmost layer of the stack are either added on - // or reduced by one, depending on if they are on the right or - // on the left side of the stack. - // - // If you are using this algorithm in your code please add - // the following line: - // - // Stack Blur Algorithm by Mario Klingemann - - final Bitmap bitmap; - if (canReuseInBitmap) { - bitmap = sentBitmap; - } else { - bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); - } - - if (radius < 1) { - return null; - } - - final int w = bitmap.getWidth(); - final int h = bitmap.getHeight(); - - final int[] pix = new int[w * h]; - bitmap.getPixels(pix, 0, w, 0, 0, w, h); - - final int wm = w - 1; - final int hm = h - 1; - final int wh = w * h; - final int div = radius + radius + 1; - - final int r[] = new int[wh]; - final int g[] = new int[wh]; - final int b[] = new int[wh]; - int rsum; - int gsum; - int bsum; - int x; - int i; - int p; - int yp; - int yi; - int yw; - final int vmin[] = new int[Math.max(w, h)]; - - int divsum = (div + 1) >> 1; - divsum *= divsum; - final int dv[] = new int[256 * divsum]; - for (i = 0; i < 256 * divsum; i++) { - dv[i] = i / divsum; - } - - yw = yi = 0; - - final int[][] stack = new int[div][3]; - int stackpointer; - int stackstart; - int[] sir; - int rbs; - final int r1 = radius + 1; - int routsum; - int goutsum; - int boutsum; - int rinsum; - int ginsum; - int binsum; - - int y; - - for (y = 0; y < h; y++) { - rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; - for (i = -radius; i <= radius; i++) { - p = pix[yi + Math.min(wm, Math.max(i, 0))]; - sir = stack[i + radius]; - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = p & 0x0000ff; - rbs = r1 - Math.abs(i); - rsum += sir[0] * rbs; - gsum += sir[1] * rbs; - bsum += sir[2] * rbs; - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - } - } - stackpointer = radius; - - for (x = 0; x < w; x++) { - - r[yi] = dv[rsum]; - g[yi] = dv[gsum]; - b[yi] = dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - - if (y == 0) { - vmin[x] = Math.min(x + radius + 1, wm); - } - p = pix[yw + vmin[x]]; - - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = p & 0x0000ff; - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[stackpointer % div]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - - yi++; - } - yw += w; - } - for (x = 0; x < w; x++) { - rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; - yp = -radius * w; - for (i = -radius; i <= radius; i++) { - yi = Math.max(0, yp) + x; - - sir = stack[i + radius]; - - sir[0] = r[yi]; - sir[1] = g[yi]; - sir[2] = b[yi]; - - rbs = r1 - Math.abs(i); - - rsum += r[yi] * rbs; - gsum += g[yi] * rbs; - bsum += b[yi] * rbs; - - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - } - - if (i < hm) { - yp += w; - } - } - yi = x; - stackpointer = radius; - for (y = 0; y < h; y++) { - // Preserve alpha channel: ( 0xff000000 & pix[yi] ) - pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - - if (x == 0) { - vmin[y] = Math.min(y + r1, hm) * w; - } - p = x + vmin[y]; - - sir[0] = r[p]; - sir[1] = g[p]; - sir[2] = b[p]; - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[stackpointer]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - - yi += w; - } - } - - bitmap.setPixels(pix, 0, w, 0, 0, w, h); - - return bitmap; - } - - private BlurUtils() { - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java b/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java deleted file mode 100644 index 9ee131d..0000000 --- a/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * 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. - * - */ - -package ru.touchin.roboswag.components.views; - -import android.content.Context; -import android.content.res.TypedArray; -import android.os.Build; -import android.support.annotation.ColorInt; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.AppCompatImageView; -import android.util.AttributeSet; -import android.util.TypedValue; - -import ru.touchin.roboswag.components.R; -import ru.touchin.roboswag.components.utils.UiUtils; - -/** - * Created by Ilia Kurtov on 07/12/2016. - * Simple endless progress bar view in material (round circle) style. - * It is able to setup size, stroke width and color. - * See MaterialLoadingBar Attributes: - * R.styleable#MaterialLoadingBar_size - * R.styleable#MaterialLoadingBar_strokeWidth - * R.styleable#MaterialLoadingBar_color - * Use - * R.styleable#MaterialLoadingBar_materialLoadingBarStyle - * to set default style of MaterialLoadingBar in your Theme. - * Sample: - * - * - */ -public class MaterialLoadingBar extends AppCompatImageView { - - private static int getPrimaryColor(@NonNull final Context context) { - final int colorAttr; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - colorAttr = android.R.attr.colorPrimary; - } else { - colorAttr = context.getResources().getIdentifier("colorPrimary", "attr", context.getPackageName()); - } - final TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute(colorAttr, outValue, true); - return outValue.data; - } - - private MaterialProgressDrawable progressDrawable; - - public MaterialLoadingBar(@NonNull final Context context) { - this(context, null); - } - - public MaterialLoadingBar(@NonNull final Context context, @Nullable final AttributeSet attrs) { - this(context, attrs, R.attr.materialLoadingBarStyle); - } - - public MaterialLoadingBar(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { - super(context, attrs, defStyleAttr); - - final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MaterialLoadingBar, - defStyleAttr, - 0); - final int size = (int) typedArray.getDimension(R.styleable.MaterialLoadingBar_size, UiUtils.OfMetrics.dpToPixels(context, 48)); - final int color = typedArray.getColor(R.styleable.MaterialLoadingBar_color, getPrimaryColor(context)); - final float strokeWidth = typedArray.getDimension(R.styleable.MaterialLoadingBar_strokeWidth, - UiUtils.OfMetrics.dpToPixels(context, 4)); - typedArray.recycle(); - - progressDrawable = new MaterialProgressDrawable(context, size); - setColor(color); - progressDrawable.setStrokeWidth(strokeWidth); - setScaleType(ScaleType.CENTER); - setImageDrawable(progressDrawable); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - progressDrawable.start(); - } - - @Override - protected void onDetachedFromWindow() { - progressDrawable.stop(); - super.onDetachedFromWindow(); - } - - /** - * Set color of loader. - * - * @param colorInt Color of loader to be set. - */ - public void setColor(@ColorInt final int colorInt) { - progressDrawable.setColor(colorInt); - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java b/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java deleted file mode 100644 index a152cb4..0000000 --- a/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * 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. - * - */ - -package ru.touchin.roboswag.components.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.Animatable; -import android.graphics.drawable.Drawable; -import android.os.SystemClock; -import android.support.annotation.ColorInt; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.touchin.roboswag.components.utils.UiUtils; - -/** - * Created by Gavriil Sitnikov on 01/03/16. - * Simple realization of endless progress bar which is looking material-like. - */ -public class MaterialProgressDrawable extends Drawable implements Runnable, Animatable { - - private static final int UPDATE_INTERVAL = 1000 / 60; - - private static final float DEFAULT_STROKE_WIDTH_DP = 4.5f; - private static final Parameters DEFAULT_PARAMETERS = new Parameters(20, 270, 4, 12, 4, 8); - - private final int size; - @NonNull - private final Paint paint; - @NonNull - private Parameters parameters = DEFAULT_PARAMETERS; - @NonNull - private final RectF arcBounds = new RectF(); - - private float rotationAngle; - private float arcSize; - private boolean running; - - public MaterialProgressDrawable(@NonNull final Context context) { - this(context, -1); - } - - public MaterialProgressDrawable(@NonNull final Context context, final int size) { - super(); - - this.size = size; - paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(UiUtils.OfMetrics.dpToPixels(context, DEFAULT_STROKE_WIDTH_DP)); - paint.setColor(Color.BLACK); - } - - @Override - public int getIntrinsicWidth() { - return size; - } - - @Override - public int getIntrinsicHeight() { - return size; - } - - /** - * Returns width of arc. - * - * @return Width. - */ - public float getStrokeWidth() { - return paint.getStrokeWidth(); - } - - /** - * Sets width of arc. - * - * @param strokeWidth Width. - */ - public void setStrokeWidth(final float strokeWidth) { - paint.setStrokeWidth(strokeWidth); - updateArcBounds(); - invalidateSelf(); - } - - /** - * Sets color of arc. - * - * @param color Color. - */ - public void setColor(@ColorInt final int color) { - paint.setColor(color); - invalidateSelf(); - } - - /** - * Returns magic parameters of spinning. - * - * @return Parameters of spinning. - */ - @NonNull - public Parameters getParameters() { - return parameters; - } - - /** - * Sets magic parameters of spinning. - * - * @param parameters Parameters of spinning. - */ - public void setParameters(@NonNull final Parameters parameters) { - this.parameters = parameters; - invalidateSelf(); - } - - @Override - protected void onBoundsChange(@NonNull final Rect bounds) { - super.onBoundsChange(bounds); - updateArcBounds(); - } - - private void updateArcBounds() { - arcBounds.set(getBounds()); - //HACK: + 1 as anti-aliasing drawing bug workaround - final int inset = (int) (paint.getStrokeWidth() / 2) + 1; - arcBounds.inset(inset, inset); - } - - @SuppressWarnings("PMD.NPathComplexity") - @Override - public void draw(@NonNull final Canvas canvas) { - final boolean isGrowingCycle = (((int) (arcSize / parameters.maxAngle)) % 2) == 0; - final float angle = arcSize % parameters.maxAngle; - final float shift = (angle / parameters.maxAngle) * parameters.gapAngle; - canvas.drawArc(arcBounds, isGrowingCycle ? rotationAngle + shift : rotationAngle + parameters.gapAngle - shift, - isGrowingCycle ? angle + parameters.gapAngle : parameters.maxAngle - angle + parameters.gapAngle, false, paint); - //TODO: compute based on animation start time - rotationAngle += isGrowingCycle ? parameters.rotationMagicNumber1 : parameters.rotationMagicNumber2; - arcSize += isGrowingCycle ? parameters.arcMagicNumber1 : parameters.arcMagicNumber2; - if (arcSize < 0) { - arcSize = 0; - } - if (isRunning()) { - scheduleSelf(this, SystemClock.uptimeMillis() + UPDATE_INTERVAL); - } - } - - @Override - public void setAlpha(final int alpha) { - paint.setAlpha(alpha); - invalidateSelf(); - } - - @Override - public void setColorFilter(@Nullable final ColorFilter colorFilter) { - paint.setColorFilter(colorFilter); - invalidateSelf(); - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - - @Override - public void start() { - if (!running) { - running = true; - run(); - } - } - - @Override - public void stop() { - if (running) { - unscheduleSelf(this); - running = false; - } - } - - @Override - public boolean isRunning() { - return running; - } - - @Override - public void run() { - if (running) { - invalidateSelf(); - } - } - - /** - * Some parameters which are using to spin progress bar. - */ - public static class Parameters { - - private final float gapAngle; - private final float maxAngle; - private final float rotationMagicNumber1; - private final float rotationMagicNumber2; - private final float arcMagicNumber1; - private final float arcMagicNumber2; - - public Parameters(final float gapAngle, final float maxAngle, - final float rotationMagicNumber1, final float rotationMagicNumber2, - final float arcMagicNumber1, final float arcMagicNumber2) { - this.gapAngle = gapAngle; - this.maxAngle = maxAngle; - this.rotationMagicNumber1 = rotationMagicNumber1; - this.rotationMagicNumber2 = rotationMagicNumber2; - this.arcMagicNumber1 = arcMagicNumber1; - this.arcMagicNumber2 = arcMagicNumber2; - } - - /** - * Returns angle of gap of arc. - * - * @return Angle of gap. - */ - public float getGapAngle() { - return gapAngle; - } - - /** - * Returns maximum angle of arc. - * - * @return Maximum angle of arc. - */ - public float getMaxAngle() { - return maxAngle; - } - - /** - * Magic parameter 1. - * - * @return Magic. - */ - public float getRotationMagicNumber1() { - return rotationMagicNumber1; - } - - /** - * Magic parameter 2. - * - * @return Magic. - */ - public float getRotationMagicNumber2() { - return rotationMagicNumber2; - } - - /** - * Magic parameter 3. - * - * @return Magic. - */ - public float getArcMagicNumber1() { - return arcMagicNumber1; - } - - /** - * Magic parameter 4. - * - * @return Magic. - */ - public float getArcMagicNumber2() { - return arcMagicNumber2; - } - - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java b/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java deleted file mode 100644 index d29b60f..0000000 --- a/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java +++ /dev/null @@ -1,345 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * 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. - * - */ - -package ru.touchin.roboswag.components.views; - -import android.content.Context; -import android.content.res.TypedArray; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.design.widget.TextInputLayout; -import android.support.v7.widget.AppCompatEditText; -import android.text.Editable; -import android.text.InputType; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.text.method.SingleLineTransformationMethod; -import android.text.method.TransformationMethod; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewParent; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; - -import java.util.ArrayList; -import java.util.List; - -import ru.touchin.roboswag.components.R; -import ru.touchin.roboswag.components.views.internal.AttributesUtils; -import ru.touchin.roboswag.core.log.Lc; - -/** - * Created by Gavriil Sitnikov on 18/07/2014. - * TextView that supports fonts from Typefaces class - */ - -/** - * Created by Gavriil Sitnikov on 18/07/2014. - * EditText that supports custom typeface and forces developer to specify if this view multiline or not. - * Also in debug mode it has common checks for popular bugs. - */ -@SuppressWarnings("PMD.ConstructorCallsOverridableMethod") -//ConstructorCallsOverridableMethod: it's ok as we need to setTypeface -public class TypefacedEditText extends AppCompatEditText { - - private static boolean inDebugMode; - - /** - * Enables debugging features like checking attributes on inflation. - */ - public static void setInDebugMode() { - inDebugMode = true; - } - - private boolean multiline; - private boolean constructed; - - @Nullable - private OnTextChangedListener onTextChangedListener; - - public TypefacedEditText(@NonNull final Context context) { - super(context); - initialize(context, null); - } - - public TypefacedEditText(@NonNull final Context context, @Nullable final AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs); - } - - public TypefacedEditText(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs); - } - - private void initialize(@NonNull final Context context, @Nullable final AttributeSet attrs) { - constructed = true; - super.setIncludeFontPadding(false); - initializeTextChangedListener(); - if (attrs != null) { - final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedEditText); - final boolean multiline = typedArray.getBoolean(R.styleable.TypefacedEditText_isMultiline, false); - if (multiline) { - setMultiline(AttributesUtils.getMaxLinesFromAttrs(context, attrs)); - } else { - setSingleLine(); - } - typedArray.recycle(); - if (inDebugMode) { - checkAttributes(context, attrs); - } - } - } - - @Nullable - public InputConnection onCreateInputConnection(@NonNull final EditorInfo attrs) { - final InputConnection inputConnection = super.onCreateInputConnection(attrs); - if (inputConnection != null && attrs.hintText == null) { - for (ViewParent parent = getParent(); parent instanceof View; parent = parent.getParent()) { - if (parent instanceof TextInputLayout) { - attrs.hintText = ((TextInputLayout) parent).getHint(); - break; - } - } - } - - return inputConnection; - } - - private void checkAttributes(@NonNull final Context context, @NonNull final AttributeSet attrs) { - final List errors = new ArrayList<>(); - Boolean multiline = null; - TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedEditText); - AttributesUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedEditText_isMultiline, true, - "isMultiline required parameter"); - if (typedArray.hasValue(R.styleable.TypefacedEditText_isMultiline)) { - multiline = typedArray.getBoolean(R.styleable.TypefacedEditText_isMultiline, false); - } - typedArray.recycle(); - - try { - final Class androidRes = Class.forName("com.android.internal.R$styleable"); - - typedArray = context.obtainStyledAttributes(attrs, AttributesUtils.getField(androidRes, "TextView")); - AttributesUtils.checkRegularTextViewAttributes(typedArray, androidRes, errors, "isMultiline"); - checkEditTextSpecificAttributes(typedArray, androidRes, errors); - if (multiline != null) { - checkMultilineAttributes(typedArray, androidRes, errors, multiline); - } - } catch (final Exception exception) { - Lc.e(exception, "Error during checking attributes"); - } - AttributesUtils.handleErrors(this, errors); - typedArray.recycle(); - } - - private void checkEditTextSpecificAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes, - @NonNull final List errors) - throws NoSuchFieldException, IllegalAccessException { - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_singleLine"), false, - "remove singleLine and use isMultiline"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_includeFontPadding"), false, - "includeFontPadding forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_ellipsize"), false, - "ellipsize forbid parameter"); - - if (typedArray.hasValue(AttributesUtils.getField(androidRes, "TextView_hint"))) { - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_textColorHint"), true, - "textColorHint required parameter if hint is not null"); - } - - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_textSize"), true, - "textSize required parameter. If it's dynamic then use '0sp'"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_inputType"), true, - "inputType required parameter"); - - final int inputType = typedArray.getInt(AttributesUtils.getField(androidRes, "TextView_inputType"), -1); - if (AttributesUtils.isNumberInputType(inputType)) { - errors.add("use inputType phone instead of number"); - } - - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_imeOptions"), true, - "imeOptions required parameter"); - } - - private void checkMultilineAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes, - @NonNull final List errors, final boolean multiline) - throws NoSuchFieldException, IllegalAccessException { - if (multiline) { - if (typedArray.getInt(AttributesUtils.getField(androidRes, "TextView_lines"), -1) == 1) { - errors.add("lines should be more than 1 if isMultiline is true"); - } - if (typedArray.getInt(AttributesUtils.getField(androidRes, "TextView_maxLines"), -1) == 1) { - errors.add("maxLines should be more than 1 if isMultiline is true"); - } - if (!typedArray.hasValue(AttributesUtils.getField(androidRes, "TextView_maxLines")) - && !typedArray.hasValue(AttributesUtils.getField(androidRes, "TextView_maxLength"))) { - errors.add("specify maxLines or maxLength if isMultiline is true"); - } - } else { - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_lines"), false, - "remove lines and use isMultiline"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_maxLines"), false, - "maxLines remove and use isMultiline"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_minLines"), false, - "minLines remove and use isMultiline"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_maxLength"), true, - "maxLength required parameter if isMultiline is false"); - } - } - - private void initializeTextChangedListener() { - addTextChangedListener(new TextWatcher() { - - @Override - public void beforeTextChanged(@NonNull final CharSequence oldText, final int start, final int count, final int after) { - //do nothing - } - - @Override - public void onTextChanged(@NonNull final CharSequence inputText, final int start, final int before, final int count) { - if (onTextChangedListener != null) { - onTextChangedListener.onTextChanged(inputText); - } - } - - @Override - public void afterTextChanged(@NonNull final Editable editable) { - //do nothing - } - - }); - } - - /** - * Sets if view supports multiline text alignment. - * - * @param maxLines Maximum lines to be set. - */ - public void setMultiline(final int maxLines) { - if (maxLines <= 1) { - Lc.assertion("Wrong maxLines: " + maxLines); - return; - } - multiline = true; - final TransformationMethod transformationMethod = getTransformationMethod(); - super.setSingleLine(false); - super.setMaxLines(maxLines); - if (!(transformationMethod instanceof SingleLineTransformationMethod)) { - setTransformationMethod(transformationMethod); - } - } - - @Override - public void setSingleLine(final boolean singleLine) { - if (singleLine) { - setSingleLine(); - } else { - setMultiline(Integer.MAX_VALUE); - } - } - - @Override - public void setSingleLine() { - final TransformationMethod transformationMethod = getTransformationMethod(); - super.setSingleLine(true); - if (transformationMethod != null) { - /*DEBUG if (!(transformationMethod instanceof SingleLineTransformationMethod)) { - Lc.w("SingleLineTransformationMethod method ignored because of previous transformation method: " + transformationMethod); - }*/ - setTransformationMethod(transformationMethod); - } - setLines(1); - multiline = false; - } - - @Override - public void setLines(final int lines) { - if (constructed && multiline && lines == 1) { - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "lines = 1 is illegal if multiline is set to true"))); - return; - } - super.setLines(lines); - } - - @Override - public void setMaxLines(final int maxLines) { - if (constructed && !multiline && maxLines > 1) { - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "maxLines > 1 is illegal if multiline is set to false"))); - return; - } - super.setMaxLines(maxLines); - } - - @Override - public void setMinLines(final int minLines) { - if (constructed && !multiline && minLines > 1) { - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "minLines > 1 is illegal if multiline is set to false"))); - return; - } - super.setMinLines(minLines); - } - - @Override - public final void setIncludeFontPadding(final boolean includeFontPadding) { - if (!constructed) { - return; - } - Lc.assertion(new IllegalStateException( - AttributesUtils.viewError(this, "Do not specify font padding as it is hard to make pixel-perfect design with such option"))); - } - - @Override - public void setEllipsize(@NonNull final TextUtils.TruncateAt ellipsis) { - if (!constructed) { - return; - } - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "Do not specify ellipsize for EditText"))); - } - - @Override - public void setInputType(final int type) { - if (AttributesUtils.isNumberInputType(type)) { - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, - "Do not specify number InputType for EditText, use phone instead"))); - super.setInputType(InputType.TYPE_CLASS_PHONE); - return; - } - super.setInputType(type); - } - - public void setOnTextChangedListener(@Nullable final OnTextChangedListener onTextChangedListener) { - this.onTextChangedListener = onTextChangedListener; - } - - /** - * Simplified variant of {@link TextWatcher}. - */ - public interface OnTextChangedListener { - - /** - * Calls when text have changed. - * - * @param text New text. - */ - void onTextChanged(@NonNull CharSequence text); - - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java b/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java deleted file mode 100644 index b8fddb2..0000000 --- a/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * 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. - * - */ - -package ru.touchin.roboswag.components.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.res.TypedArray; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.AppCompatTextView; -import android.text.TextUtils; -import android.text.method.TransformationMethod; -import android.util.AttributeSet; -import android.util.TypedValue; - -import java.util.ArrayList; -import java.util.List; - -import ru.touchin.roboswag.components.R; -import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.components.views.internal.AttributesUtils; -import ru.touchin.roboswag.core.log.Lc; - -/** - * Created by Gavriil Sitnikov on 18/07/2014. - * TextView that supports custom typeface and forces developer to specify {@link LineStrategy}. - * Also in debug mode it has common checks for popular bugs. - */ -@SuppressWarnings("PMD.ConstructorCallsOverridableMethod") -//ConstructorCallsOverridableMethod: it's ok as we need to setTypeface -public class TypefacedTextView extends AppCompatTextView { - - private static final int SIZE_THRESHOLD = 10000; - - private static final int UNSPECIFIED_MEASURE_SPEC = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - private static final int START_SCALABLE_DIFFERENCE = 4; - - private static boolean inDebugMode; - - /** - * Enables debugging features like checking attributes on inflation. - */ - public static void setInDebugMode() { - inDebugMode = true; - } - - private boolean constructed; - @NonNull - private LineStrategy lineStrategy = LineStrategy.SINGLE_LINE_ELLIPSIZE; - - public TypefacedTextView(@NonNull final Context context) { - super(context); - initialize(context, null); - } - - public TypefacedTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) { - super(context, attrs); - initialize(context, attrs); - } - - public TypefacedTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - initialize(context, attrs); - } - - private void initialize(@NonNull final Context context, @Nullable final AttributeSet attrs) { - constructed = true; - super.setIncludeFontPadding(false); - if (attrs != null) { - final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedTextView); - final LineStrategy lineStrategy = LineStrategy - .byResIndex(typedArray.getInt(R.styleable.TypefacedTextView_lineStrategy, LineStrategy.MULTILINE_ELLIPSIZE.ordinal())); - if (lineStrategy.multiline) { - setLineStrategy(lineStrategy, AttributesUtils.getMaxLinesFromAttrs(context, attrs)); - } else { - setLineStrategy(lineStrategy); - } - typedArray.recycle(); - if (inDebugMode) { - checkAttributes(context, attrs); - } - } - } - - private void checkAttributes(@NonNull final Context context, @NonNull final AttributeSet attrs) { - final List errors = new ArrayList<>(); - LineStrategy lineStrategy = null; - TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedTextView); - AttributesUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedTextView_lineStrategy, true, - "lineStrategy required parameter"); - if (typedArray.hasValue(R.styleable.TypefacedTextView_lineStrategy)) { - lineStrategy = LineStrategy.byResIndex(typedArray.getInt(R.styleable.TypefacedTextView_lineStrategy, -1)); - } - typedArray.recycle(); - - try { - final Class androidRes = Class.forName("com.android.internal.R$styleable"); - - typedArray = context.obtainStyledAttributes(attrs, AttributesUtils.getField(androidRes, "TextView")); - AttributesUtils.checkRegularTextViewAttributes(typedArray, androidRes, errors, "lineStrategy"); - checkTextViewSpecificAttributes(typedArray, androidRes, errors); - - if (lineStrategy != null) { - checkLineStrategyAttributes(typedArray, androidRes, errors, lineStrategy); - } - } catch (final Exception exception) { - Lc.e(exception, "Error during checking attributes"); - } - AttributesUtils.handleErrors(this, errors); - typedArray.recycle(); - } - - private void checkTextViewSpecificAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes, - @NonNull final List errors) - throws NoSuchFieldException, IllegalAccessException { - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_phoneNumber"), false, - "phoneNumber forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_password"), false, - "password forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_numeric"), false, - "numeric forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_inputType"), false, - "inputType forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_imeOptions"), false, - "imeOptions forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_imeActionId"), false, - "imeActionId forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_imeActionLabel"), false, - "imeActionLabel forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_hint"), false, - "hint forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_editable"), false, - "editable forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_digits"), false, - "digits forbid parameter"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_cursorVisible"), false, - "cursorVisible forbid parameter"); - } - - private void checkLineStrategyAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes, - @NonNull final List errors, @NonNull final LineStrategy lineStrategy) - throws NoSuchFieldException, IllegalAccessException { - if (!lineStrategy.scalable) { - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_textSize"), true, - "textSize required parameter. If it's dynamic then use '0sp'"); - } - if (lineStrategy.multiline) { - if (typedArray.getInt(AttributesUtils.getField(androidRes, "TextView_lines"), -1) == 1) { - errors.add("lines should be more than 1 if lineStrategy is true"); - } - if (typedArray.getInt(AttributesUtils.getField(androidRes, "TextView_maxLines"), -1) == 1) { - errors.add("maxLines should be more than 1 if lineStrategy is true"); - } - } else { - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_lines"), false, - "remove lines and use lineStrategy"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_maxLines"), false, - "remove maxLines and use lineStrategy"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_minLines"), false, - "remove minLines and use lineStrategy"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_textAllCaps"), false, - "remove textAllCaps and use app:textAllCaps"); - } - } - - /** - * Sets behavior of text if there is no space for it in one line. - * - * @param lineStrategy Specific {@link LineStrategy}. - */ - public void setLineStrategy(@NonNull final LineStrategy lineStrategy) { - setLineStrategy(lineStrategy, Integer.MAX_VALUE); - } - - /** - * Sets behavior of text if there is no space for it in one line. - * - * @param lineStrategy Specific {@link LineStrategy}; - * @param maxLines Max lines if line strategy is multiline. - */ - public void setLineStrategy(@NonNull final LineStrategy lineStrategy, final int maxLines) { - this.lineStrategy = lineStrategy; - final TransformationMethod transformationMethod = getTransformationMethod(); - super.setSingleLine(!lineStrategy.multiline); - if (transformationMethod != null) { - /*DEBUG if (!(transformationMethod instanceof SingleLineTransformationMethod)) { - Lc.w("SingleLineTransformationMethod method ignored because of previous transformation method: " + transformationMethod); - }*/ - setTransformationMethod(transformationMethod); - } - if (lineStrategy.multiline) { - super.setMaxLines(maxLines); - } - switch (lineStrategy) { - case SINGLE_LINE_ELLIPSIZE: - case MULTILINE_ELLIPSIZE: - super.setEllipsize(TextUtils.TruncateAt.END); - break; - case SINGLE_LINE_MARQUEE: - case MULTILINE_MARQUEE: - super.setEllipsize(TextUtils.TruncateAt.MARQUEE); - break; - case SINGLE_LINE_AUTO_SCALE: - super.setEllipsize(null); - break; - case SINGLE_LINE_ELLIPSIZE_MIDDLE: - case MULTILINE_ELLIPSIZE_MIDDLE: - super.setEllipsize(TextUtils.TruncateAt.MIDDLE); - break; - default: - Lc.assertion("Unknown line strategy: " + lineStrategy); - break; - } - if (lineStrategy.scalable) { - requestLayout(); - } - } - - /** - * Returns behavior of text if there is no space for it in one line. - * - * @return Specific {@link LineStrategy}. - */ - @NonNull - public LineStrategy getLineStrategy() { - return lineStrategy; - } - - @Override - public void setSingleLine() { - if (!constructed) { - return; - } - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "Do not specify setSingleLine use setLineStrategy instead"))); - } - - @Override - public void setSingleLine(final boolean singleLine) { - if (!constructed) { - return; - } - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "Do not specify setSingleLine use setLineStrategy instead"))); - } - - @Override - public void setLines(final int lines) { - if (constructed && lineStrategy.multiline && lines == 1) { - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "lines = 1 is illegal if lineStrategy is multiline"))); - return; - } - super.setLines(lines); - } - - @Override - public void setMaxLines(final int maxLines) { - if (constructed && !lineStrategy.multiline && maxLines > 1) { - Lc.assertion(new IllegalStateException( - AttributesUtils.viewError(this, "maxLines > 1 is illegal if lineStrategy is single line"))); - return; - } - super.setMaxLines(maxLines); - } - - @Override - public final void setIncludeFontPadding(final boolean includeFontPadding) { - if (!constructed) { - return; - } - Lc.assertion(new IllegalStateException( - AttributesUtils.viewError(this, "Do not specify font padding as it is hard to make pixel-perfect design with such option"))); - } - - @Override - public void setMinLines(final int minLines) { - if (constructed && !lineStrategy.multiline && minLines > 1) { - Lc.assertion(new IllegalStateException( - AttributesUtils.viewError(this, "minLines > 1 is illegal if lineStrategy is single line"))); - return; - } - super.setMinLines(minLines); - } - - @Override - public void setEllipsize(@NonNull final TextUtils.TruncateAt ellipsize) { - if (!constructed) { - return; - } - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "Do not specify ellipsize use setLineStrategy instead"))); - } - - @Override - public void setText(@Nullable final CharSequence text, @Nullable final BufferType type) { - super.setText(text, type); - if (constructed && lineStrategy.scalable) { - requestLayout(); - } - } - - @Override - public void setTextSize(final float size) { - if (constructed && lineStrategy.scalable) { - Lc.cutAssertion(new IllegalStateException(AttributesUtils.viewError(this, "textSize call is illegal if lineStrategy is scalable"))); - return; - } - super.setTextSize(size); - } - - @Override - public void setTextSize(final int unit, final float size) { - if (constructed && lineStrategy.scalable) { - Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "textSize call is illegal if lineStrategy is scalable"))); - return; - } - super.setTextSize(unit, size); - } - - @SuppressLint("WrongCall") - //WrongCall: actually this method is always calling from onMeasure - private void computeScalableTextSize(final int maxWidth, final int maxHeight) { - final int minDifference = (int) UiUtils.OfMetrics.dpToPixels(getContext(), 1); - int difference = (int) UiUtils.OfMetrics.dpToPixels(getContext(), START_SCALABLE_DIFFERENCE); - ScaleAction scaleAction = ScaleAction.DO_NOTHING; - ScaleAction previousScaleAction = ScaleAction.DO_NOTHING; - do { - switch (scaleAction) { - case SCALE_DOWN: - if (difference > minDifference) { - difference -= minDifference; - super.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(0, getTextSize() - difference)); - } else { - super.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(0, getTextSize() - minDifference)); - if (previousScaleAction == ScaleAction.SCALE_UP) { - return; - } - } - break; - case SCALE_UP: - if (difference > minDifference) { - difference -= minDifference; - super.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(0, getTextSize() + difference)); - } else { - if (previousScaleAction == ScaleAction.SCALE_DOWN) { - return; - } - super.setTextSize(TypedValue.COMPLEX_UNIT_PX, Math.max(0, getTextSize() + minDifference)); - } - break; - case DO_NOTHING: - default: - break; - } - super.onMeasure(UNSPECIFIED_MEASURE_SPEC, UNSPECIFIED_MEASURE_SPEC); - previousScaleAction = scaleAction; - scaleAction = computeScaleAction(maxWidth, maxHeight); - } - while (scaleAction != ScaleAction.DO_NOTHING); - } - - @NonNull - private ScaleAction computeScaleAction(final int maxWidth, final int maxHeight) { - ScaleAction result = ScaleAction.DO_NOTHING; - if (maxWidth < getMeasuredWidth()) { - result = ScaleAction.SCALE_DOWN; - } else if (maxWidth > getMeasuredWidth()) { - result = ScaleAction.SCALE_UP; - } - - if (maxHeight < getMeasuredHeight()) { - result = ScaleAction.SCALE_DOWN; - } else if (maxHeight > getMeasuredHeight() && result != ScaleAction.SCALE_DOWN) { - result = ScaleAction.SCALE_UP; - } - return result; - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - final int maxWidth = MeasureSpec.getSize(widthMeasureSpec); - final int maxHeight = MeasureSpec.getSize(heightMeasureSpec); - if (!constructed || !lineStrategy.scalable || (maxWidth <= 0 && maxHeight <= 0) || TextUtils.isEmpty(getText())) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - - computeScalableTextSize(MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED ? maxWidth : SIZE_THRESHOLD, - MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED ? maxHeight : SIZE_THRESHOLD); - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - private enum ScaleAction { - SCALE_DOWN, - SCALE_UP, - DO_NOTHING - } - - /** - * Specific behavior, mostly based on combination of {@link #getEllipsize()} and {@link #getMaxLines()} to specify how view should show text - * if there is no space for it on one line. - */ - public enum LineStrategy { - - /** - * Not more than one line and ellipsize text with dots at the end. - */ - SINGLE_LINE_ELLIPSIZE(false, false), - /** - * Not more than one line and ellipsize text with marquee at the end. - */ - SINGLE_LINE_MARQUEE(false, false), - /** - * Not more than one line and scale text to maximum possible size. - */ - SINGLE_LINE_AUTO_SCALE(false, true), - /** - * More than one line and ellipsize text with dots at the end. - */ - MULTILINE_ELLIPSIZE(true, false), - /** - * More than one line and ellipsize text with marquee at the end. - */ - MULTILINE_MARQUEE(true, false), - /** - * Not more than one line and ellipsize text with dots in the middle. - */ - SINGLE_LINE_ELLIPSIZE_MIDDLE(false, false), - /** - * More than one line and ellipsize text with dots in the middle. - */ - MULTILINE_ELLIPSIZE_MIDDLE(true, false); - - @NonNull - public static LineStrategy byResIndex(final int resIndex) { - if (resIndex < 0 || resIndex >= values().length) { - Lc.assertion("Unexpected resIndex " + resIndex); - return MULTILINE_ELLIPSIZE; - } - return values()[resIndex]; - } - - private final boolean multiline; - private final boolean scalable; - - LineStrategy(final boolean multiline, final boolean scalable) { - this.multiline = multiline; - this.scalable = scalable; - } - - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java b/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java deleted file mode 100644 index 6965e72..0000000 --- a/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * 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. - * - */ - -package ru.touchin.roboswag.components.views.internal; - -import android.content.Context; -import android.content.res.TypedArray; -import android.support.annotation.NonNull; -import android.support.annotation.StyleableRes; -import android.text.InputType; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.View; - -import java.lang.reflect.Field; -import java.util.Collection; - -import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; - -/** - * Created by Gavriil Sitnikov on 13/06/2016. - * Bunch of inner helper library methods to validate attributes of custom views. - */ -public final class AttributesUtils { - - /** - * Gets static field of class. - * - * @param resourcesClass Class to get field from; - * @param fieldName name of field; - * @param Type of object that is stored in field; - * @return Field value; - * @throws NoSuchFieldException Throws on reflection call; - * @throws IllegalAccessException Throws on reflection call. - */ - @NonNull - @SuppressWarnings("unchecked") - public static T getField(@NonNull final Class resourcesClass, @NonNull final String fieldName) - throws NoSuchFieldException, IllegalAccessException { - final Field field = resourcesClass.getDeclaredField(fieldName); - field.setAccessible(true); - return (T) field.get(null); - } - - /** - * Checks if attribute is in array or not and collecterror if attribute missed. - * - * @param typedArray Array of attributes; - * @param errors Errors to collect into; - * @param resourceId Id of attribute; - * @param required Is parameter have to be in array OR it have not to be in; - * @param description Description of error. - */ - public static void checkAttribute(@NonNull final TypedArray typedArray, - @NonNull final Collection errors, - @StyleableRes final int resourceId, - final boolean required, - @NonNull final String description) { - if ((required && typedArray.hasValue(resourceId)) - || (!required && !typedArray.hasValue(resourceId))) { - return; - } - errors.add(description); - } - - /** - * Collects regular {@link android.widget.TextView} errors. - * - * @param typedArray Array of attributes; - * @param androidRes Class of styleable attributes; - * @param errors Errors to collect into; - * @param lineStrategyParameterName name of line strategy parameter; - * @throws NoSuchFieldException Throws during getting attribute values through reflection; - * @throws IllegalAccessException Throws during getting attribute values through reflection. - */ - public static void checkRegularTextViewAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes, - @NonNull final Collection errors, @NonNull final String lineStrategyParameterName) - throws NoSuchFieldException, IllegalAccessException { - checkAttribute(typedArray, errors, getField(androidRes, "TextView_fontFamily"), true, "fontFamily required parameter"); - checkAttribute(typedArray, errors, getField(androidRes, "TextView_includeFontPadding"), false, "includeFontPadding forbid parameter"); - checkAttribute(typedArray, errors, getField(androidRes, "TextView_singleLine"), false, - "remove singleLine and use " + lineStrategyParameterName); - checkAttribute(typedArray, errors, getField(androidRes, "TextView_ellipsize"), false, - "remove ellipsize and use " + lineStrategyParameterName); - checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_textColor"), true, - "textColor required parameter. If it's dynamic then use 'android:color/transparent'"); - } - - /** - * Inner helper library method to merge errors in string and assert it. - * - * @param view View with errors; - * @param errors Errors of view. - */ - public static void handleErrors(@NonNull final View view, @NonNull final Collection errors) { - if (!errors.isEmpty()) { - final String exceptionText = viewError(view, TextUtils.join("\n", errors)); - Lc.cutAssertion(new ShouldNotHappenException(exceptionText)); - } - } - - /** - * Returns max lines attribute value for views extended from {@link android.widget.TextView}. - * - * @param context Context of attributes; - * @param attrs TextView based attributes; - * @return Max lines value. - */ - public static int getMaxLinesFromAttrs(@NonNull final Context context, @NonNull final AttributeSet attrs) { - try { - final Class androidRes = Class.forName("com.android.internal.R$styleable"); - final TypedArray typedArray = context.obtainStyledAttributes(attrs, AttributesUtils.getField(androidRes, "TextView")); - final int result = typedArray.getInt(AttributesUtils.getField(androidRes, "TextView_maxLines"), Integer.MAX_VALUE); - typedArray.recycle(); - return result; - } catch (final Exception exception) { - return Integer.MAX_VALUE; - } - } - - /** - * Creates readable view error. - * - * @param view View of error; - * @param errorText Text of error; - * @return Readable error string. - */ - @NonNull - public static String viewError(@NonNull final View view, @NonNull final String errorText) { - return "Errors for view id=" + UiUtils.OfViews.getViewIdString(view) + ":\n" + errorText; - } - - /** - * Returns true if input type equals number input type. - * - * @param inputType Input type to check; - * @return true if input type equals number input type. - */ - public static boolean isNumberInputType(final int inputType) { - return inputType == InputType.TYPE_CLASS_NUMBER || inputType == InputType.TYPE_DATETIME_VARIATION_NORMAL; - } - - private AttributesUtils() { - } - -} diff --git a/src/main/res/anim/fragment_slide_in_left_paralax_animation.xml b/src/main/res/anim/fragment_slide_in_left_paralax_animation.xml deleted file mode 100644 index 2a638ac..0000000 --- a/src/main/res/anim/fragment_slide_in_left_paralax_animation.xml +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/main/res/anim/fragment_slide_in_right_animation.xml b/src/main/res/anim/fragment_slide_in_right_animation.xml deleted file mode 100644 index 76be496..0000000 --- a/src/main/res/anim/fragment_slide_in_right_animation.xml +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/main/res/anim/fragment_slide_out_left_paralax_animation.xml b/src/main/res/anim/fragment_slide_out_left_paralax_animation.xml deleted file mode 100644 index 67b35b0..0000000 --- a/src/main/res/anim/fragment_slide_out_left_paralax_animation.xml +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/main/res/anim/fragment_slide_out_right_animation.xml b/src/main/res/anim/fragment_slide_out_right_animation.xml deleted file mode 100644 index 9757e23..0000000 --- a/src/main/res/anim/fragment_slide_out_right_animation.xml +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/main/res/anim/global_fade_in_animation.xml b/src/main/res/anim/global_fade_in_animation.xml deleted file mode 100644 index 200a2bf..0000000 --- a/src/main/res/anim/global_fade_in_animation.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_fade_out_animation.xml b/src/main/res/anim/global_fade_out_animation.xml deleted file mode 100644 index 472546a..0000000 --- a/src/main/res/anim/global_fade_out_animation.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_slide_in_left_animation.xml b/src/main/res/anim/global_slide_in_left_animation.xml deleted file mode 100644 index 9ddb859..0000000 --- a/src/main/res/anim/global_slide_in_left_animation.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_slide_in_right_animation.xml b/src/main/res/anim/global_slide_in_right_animation.xml deleted file mode 100644 index ac7f27a..0000000 --- a/src/main/res/anim/global_slide_in_right_animation.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_slide_out_left_animation.xml b/src/main/res/anim/global_slide_out_left_animation.xml deleted file mode 100644 index 5bc1921..0000000 --- a/src/main/res/anim/global_slide_out_left_animation.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_slide_out_right_animation.xml b/src/main/res/anim/global_slide_out_right_animation.xml deleted file mode 100644 index 0839498..0000000 --- a/src/main/res/anim/global_slide_out_right_animation.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/drawable-v21/global_dark_selector.xml b/src/main/res/drawable-v21/global_dark_selector.xml deleted file mode 100644 index 847128c..0000000 --- a/src/main/res/drawable-v21/global_dark_selector.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/main/res/drawable-v21/global_light_selector.xml b/src/main/res/drawable-v21/global_light_selector.xml deleted file mode 100644 index b0932fd..0000000 --- a/src/main/res/drawable-v21/global_light_selector.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/main/res/drawable/global_dark_selector.xml b/src/main/res/drawable/global_dark_selector.xml deleted file mode 100644 index 1a825ed..0000000 --- a/src/main/res/drawable/global_dark_selector.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/main/res/drawable/global_light_selector.xml b/src/main/res/drawable/global_light_selector.xml deleted file mode 100644 index 39c33f0..0000000 --- a/src/main/res/drawable/global_light_selector.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml deleted file mode 100644 index 7320080..0000000 --- a/src/main/res/values/attrs.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/res/values/integers.xml b/src/main/res/values/integers.xml deleted file mode 100644 index 93ec0fc..0000000 --- a/src/main/res/values/integers.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 250 - diff --git a/storable/.gitignore b/storable/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/storable/.gitignore @@ -0,0 +1 @@ +/build diff --git a/storable/build.gradle b/storable/build.gradle new file mode 100644 index 0000000..2a6be21 --- /dev/null +++ b/storable/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion 16 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":utils") + api project(":logging") + + implementation "com.android.support:support-annotations:$versions.supportLibrary" + + implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" + implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid" +} diff --git a/storable/src/main/AndroidManifest.xml b/storable/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fd2722a --- /dev/null +++ b/storable/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java similarity index 99% rename from src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java rename to storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java index caa718a..73d90bf 100644 --- a/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java +++ b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java @@ -31,7 +31,6 @@ import ru.touchin.roboswag.core.utils.Optional; import io.reactivex.Completable; import io.reactivex.Single; - /** * Created by Gavriil Sitnikov on 18/03/16. * Store based on {@link SharedPreferences} for {@link ru.touchin.roboswag.core.observables.storable.Storable}. diff --git a/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java similarity index 61% rename from src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java rename to storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java index 2ee1db9..5ae4521 100644 --- a/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java +++ b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java @@ -45,9 +45,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable stringStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, String.class, - String.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + String.class, + String.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -59,13 +63,18 @@ public final class PreferenceUtils { * @return {@link Storable} for string. */ @NonNull - public static NonNullStorable stringStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - @NonNull final String defaultValue) { - return new Storable.Builder(name, String.class, - String.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable stringStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + @NonNull final String defaultValue + ) { + return new Storable.Builder( + name, + String.class, + String.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -77,9 +86,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable longStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, Long.class, - Long.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + Long.class, + Long.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -91,13 +104,18 @@ public final class PreferenceUtils { * @return {@link Storable} for long. */ @NonNull - public static NonNullStorable longStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - final long defaultValue) { - return new Storable.Builder(name, Long.class, - Long.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable longStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + final long defaultValue + ) { + return new Storable.Builder( + name, + Long.class, + Long.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -109,9 +127,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable booleanStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, Boolean.class, - Boolean.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + Boolean.class, + Boolean.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -123,13 +145,18 @@ public final class PreferenceUtils { * @return {@link Storable} for boolean. */ @NonNull - public static NonNullStorable booleanStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - final boolean defaultValue) { - return new Storable.Builder(name, Boolean.class, - Boolean.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable booleanStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + final boolean defaultValue + ) { + return new Storable.Builder( + name, + Boolean.class, + Boolean.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -141,9 +168,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable integerStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, Integer.class, - Integer.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + Integer.class, + Integer.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -155,13 +186,18 @@ public final class PreferenceUtils { * @return {@link Storable} for integer. */ @NonNull - public static NonNullStorable integerStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - final int defaultValue) { - return new Storable.Builder(name, Integer.class, - Integer.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable integerStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + final int defaultValue + ) { + return new Storable.Builder( + name, + Integer.class, + Integer.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -173,9 +209,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable floatStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, Float.class, - Float.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + Float.class, + Float.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -187,13 +227,18 @@ public final class PreferenceUtils { * @return {@link Storable} for float. */ @NonNull - public static NonNullStorable floatStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - final float defaultValue) { - return new Storable.Builder(name, Float.class, - Float.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable floatStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + final float defaultValue + ) { + return new Storable.Builder( + name, + Float.class, + Float.class, + new PreferenceStore(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -204,12 +249,18 @@ public final class PreferenceUtils { * @return {@link Storable} for enum. */ @NonNull - public static > Storable enumStorable(@NonNull final String name, - @NonNull final Class enumClass, - @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, enumClass, - String.class, new PreferenceStore<>(preferences), new EnumToStringConverter<>()) - .build(); + public static > Storable enumStorable( + @NonNull final String name, + @NonNull final Class enumClass, + @NonNull final SharedPreferences preferences + ) { + return new Storable.Builder( + name, + enumClass, + String.class, + new PreferenceStore(preferences), + new EnumToStringConverter<>() + ).build(); } /** @@ -221,14 +272,19 @@ public final class PreferenceUtils { * @return {@link Storable} for enum. */ @NonNull - public static > NonNullStorable enumStorable(@NonNull final String name, - @NonNull final Class enumClass, - @NonNull final SharedPreferences preferences, - @NonNull final T defaultValue) { - return new Storable.Builder(name, enumClass, - String.class, new PreferenceStore<>(preferences), new EnumToStringConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static > NonNullStorable enumStorable( + @NonNull final String name, + @NonNull final Class enumClass, + @NonNull final SharedPreferences preferences, + @NonNull final T defaultValue + ) { + return new Storable.Builder( + name, + enumClass, + String.class, + new PreferenceStore(preferences), + new EnumToStringConverter<>() + ).setDefaultValue(defaultValue).build(); } private PreferenceUtils() { diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/ObservableRefCountWithCacheTime.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/ObservableRefCountWithCacheTime.java new file mode 100644 index 0000000..6217204 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/ObservableRefCountWithCacheTime.java @@ -0,0 +1,299 @@ +/* + Copyright (c) 2016-present, RxJava Contributors. +

+ 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. + */ + +package ru.touchin.roboswag.core.observables; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.Observer; +import io.reactivex.Scheduler; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; +import io.reactivex.functions.Consumer; +import io.reactivex.internal.disposables.DisposableHelper; +import io.reactivex.internal.fuseable.HasUpstreamObservableSource; +import io.reactivex.observables.ConnectableObservable; +import io.reactivex.schedulers.Schedulers; + +/** + * Returns an observable sequence that stays connected to the source as long as + * there is at least one subscription to the observable sequence. + * + * @param the value type + */ +@SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.AvoidUsingVolatile"}) +//AvoidUsingVolatile: it's RxJava code +public final class ObservableRefCountWithCacheTime extends Observable implements HasUpstreamObservableSource { + + @NonNull + private final ConnectableObservable connectableSource; + @NonNull + private final ObservableSource actualSource; + + @NonNull + private volatile CompositeDisposable baseDisposable = new CompositeDisposable(); + + @NonNull + private final AtomicInteger subscriptionCount = new AtomicInteger(); + + /** + * Use this lock for every subscription and disconnect action. + */ + @NonNull + private final ReentrantLock lock = new ReentrantLock(); + + @NonNull + private final Scheduler scheduler = Schedulers.computation(); + private final long cacheTime; + @NonNull + private final TimeUnit cacheTimeUnit; + @Nullable + private Scheduler.Worker worker; + + /** + * Constructor. + * + * @param source observable to apply ref count to + */ + public ObservableRefCountWithCacheTime(@NonNull final ConnectableObservable source, + final long cacheTime, @NonNull final TimeUnit cacheTimeUnit) { + super(); + this.connectableSource = source; + this.actualSource = source; + this.cacheTime = cacheTime; + this.cacheTimeUnit = cacheTimeUnit; + } + + @NonNull + public ObservableSource source() { + return actualSource; + } + + private void cleanupWorker() { + if (worker != null) { + worker.dispose(); + worker = null; + } + } + + @Override + public void subscribeActual(@NonNull final Observer subscriber) { + + lock.lock(); + if (subscriptionCount.incrementAndGet() == 1) { + cleanupWorker(); + final AtomicBoolean writeLocked = new AtomicBoolean(true); + + try { + // need to use this overload of connect to ensure that + // baseDisposable is set in the case that source is a + // synchronous Observable + connectableSource.connect(onSubscribe(subscriber, writeLocked)); + } finally { + // need to cover the case where the source is subscribed to + // outside of this class thus preventing the Action1 passed + // to source.connect above being called + if (writeLocked.get()) { + // Action1 passed to source.connect was not called + lock.unlock(); + } + } + } else { + try { + // ready to subscribe to source so do it + doSubscribe(subscriber, baseDisposable); + } finally { + // release the read lock + lock.unlock(); + } + } + + } + + @NonNull + private Consumer onSubscribe(@NonNull final Observer observer, @NonNull final AtomicBoolean writeLocked) { + return new DisposeConsumer(observer, writeLocked); + } + + private void doSubscribe(@NonNull final Observer observer, @NonNull final CompositeDisposable currentBase) { + // handle disposing from the base CompositeDisposable + final Disposable disposable = disconnect(currentBase); + + final ConnectionObserver connectionObserver = new ConnectionObserver(observer, currentBase, disposable); + observer.onSubscribe(connectionObserver); + + connectableSource.subscribe(connectionObserver); + } + + @NonNull + private Disposable disconnect(@NonNull final CompositeDisposable current) { + return Disposables.fromRunnable(new DisposeTask(current)); + } + + private final class ConnectionObserver extends AtomicReference implements Observer, Disposable { + + private static final long serialVersionUID = 3813126992133394324L; + + @NonNull + private final Observer subscriber; + @NonNull + private final CompositeDisposable currentBase; + @NonNull + private final Disposable resource; + + public ConnectionObserver(@NonNull final Observer subscriber, @NonNull final CompositeDisposable currentBase, + @NonNull final Disposable resource) { + super(); + this.subscriber = subscriber; + this.currentBase = currentBase; + this.resource = resource; + } + + @Override + public void onSubscribe(@NonNull final Disposable disposable) { + DisposableHelper.setOnce(this, disposable); + } + + @Override + public void onError(@NonNull final Throwable throwable) { + cleanup(); + subscriber.onError(throwable); + } + + @Override + public void onNext(@NonNull final T item) { + subscriber.onNext(item); + } + + @Override + public void onComplete() { + cleanup(); + subscriber.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + resource.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + private void cleanup() { + // on error or completion we need to dispose the base CompositeDisposable + // and set the subscriptionCount to 0 + lock.lock(); + try { + if (baseDisposable == currentBase) { + cleanupWorker(); + if (connectableSource instanceof Disposable) { + ((Disposable) connectableSource).dispose(); + } + + baseDisposable.dispose(); + baseDisposable = new CompositeDisposable(); + subscriptionCount.set(0); + } + } finally { + lock.unlock(); + } + } + + } + + private final class DisposeConsumer implements Consumer { + + @NonNull + private final Observer observer; + @NonNull + private final AtomicBoolean writeLocked; + + public DisposeConsumer(@NonNull final Observer observer, @NonNull final AtomicBoolean writeLocked) { + this.observer = observer; + this.writeLocked = writeLocked; + } + + @Override + public void accept(@NonNull final Disposable subscription) { + try { + baseDisposable.add(subscription); + // ready to subscribe to source so do it + doSubscribe(observer, baseDisposable); + } finally { + // release the write lock + lock.unlock(); + writeLocked.set(false); + } + } + + } + + private final class DisposeTask implements Runnable { + + @NonNull + private final CompositeDisposable current; + + public DisposeTask(@NonNull final CompositeDisposable current) { + this.current = current; + } + + @Override + public void run() { + lock.lock(); + try { + if (baseDisposable == current && subscriptionCount.decrementAndGet() == 0) { + if (worker != null) { + worker.dispose(); + } else { + worker = scheduler.createWorker(); + } + worker.schedule(() -> { + lock.lock(); + try { + if (subscriptionCount.get() == 0) { + cleanupWorker(); + if (connectableSource instanceof Disposable) { + ((Disposable) connectableSource).dispose(); + } + + baseDisposable.dispose(); + // need a new baseDisposable because once + // disposed stays that way + baseDisposable = new CompositeDisposable(); + } + } finally { + lock.unlock(); + } + }, cacheTime, cacheTimeUnit); + } + } finally { + lock.unlock(); + } + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/BaseStorable.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/BaseStorable.java new file mode 100644 index 0000000..390a024 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/BaseStorable.java @@ -0,0 +1,472 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.Single; +import io.reactivex.plugins.RxJavaPlugins; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.PublishSubject; +import ru.touchin.roboswag.core.log.LcGroup; +import ru.touchin.roboswag.core.observables.ObservableRefCountWithCacheTime; +import ru.touchin.roboswag.core.utils.ObjectUtils; +import ru.touchin.roboswag.core.utils.Optional; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Base class allows to async access to some store. + * Supports conversion between store and actual value. If it is not needed then use {@link SameTypesConverter} + * Supports migration from specific version to latest by {@link Migration} object. + * Allows to set default value which will be returned if actual value is null. + * Allows to declare specific {@link ObserveStrategy}. + * Also specific {@link Scheduler} could be specified to not create new scheduler per storable. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}; + * @param Type of actual value operating by Storable. Could be same as {@link TObject}. + */ +public abstract class BaseStorable { + + public static final LcGroup STORABLE_LC_GROUP = new LcGroup("STORABLE"); + + private static final long DEFAULT_CACHE_TIME_MILLIS = TimeUnit.SECONDS.toMillis(5); + + @NonNull + private static ObserveStrategy getDefaultObserveStrategyFor(@NonNull final Type objectType, @NonNull final Type storeObjectType) { + if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) objectType)) { + return ObserveStrategy.CACHE_ACTUAL_VALUE; + } + if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) storeObjectType)) { + return ObserveStrategy.CACHE_STORE_VALUE; + } + return ObserveStrategy.NO_CACHE; + } + + @NonNull + private final TKey key; + @NonNull + private final Type objectType; + @NonNull + private final Type storeObjectType; + @NonNull + private final Store store; + @NonNull + private final Converter converter; + @NonNull + private final PublishSubject> newStoreValueEvent = PublishSubject.create(); + @NonNull + private final Observable> storeValueObservable; + @NonNull + private final Observable> valueObservable; + @NonNull + private final Scheduler scheduler; + + public BaseStorable(@NonNull final BuilderCore builderCore) { + this(builderCore.key, builderCore.objectType, builderCore.storeObjectType, + builderCore.store, builderCore.converter, builderCore.observeStrategy, + builderCore.migration, builderCore.defaultValue, builderCore.storeScheduler, builderCore.cacheTimeMillis); + } + + @SuppressWarnings("PMD.ExcessiveParameterList") + //ExcessiveParameterList: that's why we are using builder to create it + private BaseStorable(@NonNull final TKey key, + @NonNull final Type objectType, + @NonNull final Type storeObjectType, + @NonNull final Store store, + @NonNull final Converter converter, + @Nullable final ObserveStrategy observeStrategy, + @Nullable final Migration migration, + @Nullable final TObject defaultValue, + @Nullable final Scheduler storeScheduler, + final long cacheTimeMillis) { + this.key = key; + this.objectType = objectType; + this.storeObjectType = storeObjectType; + this.store = store; + this.converter = converter; + + final ObserveStrategy nonNullObserveStrategy + = observeStrategy != null ? observeStrategy : getDefaultObserveStrategyFor(objectType, storeObjectType); + scheduler = storeScheduler != null ? storeScheduler : Schedulers.from(Executors.newSingleThreadExecutor()); + storeValueObservable + = createStoreValueObservable(nonNullObserveStrategy, migration, defaultValue, cacheTimeMillis); + valueObservable = createValueObservable(storeValueObservable, nonNullObserveStrategy, cacheTimeMillis); + } + + @Nullable + private Optional returnDefaultValueIfNull(@NonNull final Optional storeObject, @Nullable final TObject defaultValue) + throws Converter.ConversionException { + if (storeObject.get() != null || defaultValue == null) { + return storeObject; + } + + try { + return new Optional<>(converter.toStoreObject(objectType, storeObjectType, defaultValue)); + } catch (final Converter.ConversionException exception) { + STORABLE_LC_GROUP.w(exception, "Exception while converting default value of '%s' from '%s' from store %s", + key, defaultValue, store); + throw exception; + } + } + + @NonNull + private Observable> createStoreInitialLoadingObservable(@Nullable final Migration migration) { + final Single> loadObservable = store.loadObject(storeObjectType, key) + .doOnError(throwable -> STORABLE_LC_GROUP.w(throwable, "Exception while trying to load value of '%s' from store %s", key, store)); + return (migration != null ? migration.migrateToLatestVersion(key).andThen(loadObservable) : loadObservable) + .subscribeOn(scheduler) + .observeOn(scheduler) + .toObservable() + .replay(1) + .refCount() + .take(1); + } + + @NonNull + private Observable> createStoreValueObservable(@NonNull final ObserveStrategy observeStrategy, + @Nullable final Migration migration, + @Nullable final TObject defaultValue, + final long cacheTimeMillis) { + final Observable> storeInitialLoadingObservable = createStoreInitialLoadingObservable(migration); + final Observable> result = storeInitialLoadingObservable + .concatWith(newStoreValueEvent) + .map(storeObject -> returnDefaultValueIfNull(storeObject, defaultValue)); + return observeStrategy == ObserveStrategy.CACHE_STORE_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE + ? RxJavaPlugins.onAssembly(new ObservableRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS)) + : result; + } + + @NonNull + private Observable> createValueObservable(@NonNull final Observable> storeValueObservable, + @NonNull final ObserveStrategy observeStrategy, + final long cacheTimeMillis) { + final Observable> result = storeValueObservable + .map(storeObject -> { + try { + return new Optional<>(converter.toObject(objectType, storeObjectType, storeObject.get())); + } catch (final Converter.ConversionException exception) { + STORABLE_LC_GROUP.w(exception, "Exception while trying to converting value of '%s' from store %s by %s", + key, storeObject, store, converter); + throw exception; + } + }); + return observeStrategy == ObserveStrategy.CACHE_ACTUAL_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE + ? RxJavaPlugins.onAssembly(new ObservableRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS)) + : result; + } + + /** + * Returns key of value. + * + * @return Unique key. + */ + @NonNull + public TKey getKey() { + return key; + } + + /** + * Returns type of actual object. + * + * @return Type of actual object. + */ + @NonNull + public Type getObjectType() { + return objectType; + } + + /** + * Returns type of store object. + * + * @return Type of store object. + */ + @NonNull + public Type getStoreObjectType() { + return storeObjectType; + } + + /** + * Returns {@link Store} where store class representation of object is storing. + * + * @return Store. + */ + @NonNull + public Store getStore() { + return store; + } + + /** + * Returns {@link Converter} to convert values from store class to actual and back. + * + * @return Converter. + */ + @NonNull + public Converter getConverter() { + return converter; + } + + @NonNull + private Completable internalSet(@Nullable final TObject newValue, final boolean checkForEqualityBeforeSet) { + return (checkForEqualityBeforeSet ? storeValueObservable.firstOrError() : Single.just(new Optional<>(null))) + .observeOn(scheduler) + .flatMapCompletable(oldStoreValue -> { + final TStoreObject newStoreValue; + try { + newStoreValue = converter.toStoreObject(objectType, storeObjectType, newValue); + } catch (final Converter.ConversionException exception) { + STORABLE_LC_GROUP.w(exception, "Exception while trying to store value of '%s' from store %s by %s", + key, newValue, store, converter); + return Completable.error(exception); + } + if (checkForEqualityBeforeSet && ObjectUtils.equals(newStoreValue, oldStoreValue.get())) { + return Completable.complete(); + } + return store.storeObject(storeObjectType, key, newStoreValue) + .doOnError(throwable -> STORABLE_LC_GROUP.w(throwable, + "Exception while trying to store value of '%s' from store %s by %s", + key, newValue, store, converter)) + .observeOn(scheduler) + .andThen(Completable.fromAction(() -> { + newStoreValueEvent.onNext(new Optional<>(newStoreValue)); + if (checkForEqualityBeforeSet) { + STORABLE_LC_GROUP.i("Value of '%s' changed from '%s' to '%s'", key, oldStoreValue, newStoreValue); + } else { + STORABLE_LC_GROUP.i("Value of '%s' force changed to '%s'", key, newStoreValue); + } + })); + }); + } + + /** + * Creates observable which is async setting value to store. + * It is not checking if stored value equals new value. + * In result it will be faster to not get value from store and compare but it will emit item to {@link #observe()} subscribers. + * NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! + * + * @param newValue Value to set; + * @return Observable of setting process. + */ + @NonNull + public Completable forceSet(@Nullable final TObject newValue) { + return internalSet(newValue, false); + } + + /** + * Creates observable which is async setting value to store. + * It is checking if stored value equals new value. + * In result it will take time to get value from store and compare + * but it won't emit item to {@link #observe()} subscribers if stored value equals new value. + * NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! + * + * @param newValue Value to set; + * @return Observable of setting process. + */ + @NonNull + public Completable set(@Nullable final TObject newValue) { + return internalSet(newValue, true); + } + + /** + * Sets value synchronously. You should NOT use this method normally. Use {@link #set(Object)} asynchronously instead. + * + * @param newValue Value to set; + */ + @Deprecated + //deprecation: it should be used for debug only and in very rare cases. + public void setSync(@Nullable final TObject newValue) { + set(newValue).blockingAwait(); + } + + @NonNull + protected Observable> observeOptionalValue() { + return valueObservable; + } + + /** + * Returns Observable which is emitting item on subscribe and every time when someone have changed value. + * It could emit next and error events but not completed. + * + * @return Returns observable of value. + */ + @NonNull + public abstract Observable observe(); + + /** + * Returns Observable which is emitting only one item on subscribe. + * It could emit next and error events but not completed. + * + * @return Returns observable of value. + */ + @NonNull + public Single get() { + return observe().firstOrError(); + } + + /** + * Gets value synchronously. You should NOT use this method normally. Use {@link #get()} or {@link #observe()} asynchronously instead. + * + * @return Returns value; + */ + @Deprecated + //deprecation: it should be used for debug only and in very rare cases. + @Nullable + public TReturnObject getSync() { + return get().blockingGet(); + } + + /** + * Enum that is representing strategy of observing item from store. + */ + public enum ObserveStrategy { + + /** + * Not caching value so on every {@link #get()} emit it will get value from {@link #getStore()} and converts it with {@link #getConverter()}. + */ + NO_CACHE, + /** + * Caching only store value so on every {@link #get()} emit it will converts it with {@link #getConverter()}. + * Do not use such strategy if store object could be big (like byte-array of file). + */ + CACHE_STORE_VALUE, + /** + * Caching value so it won't spend time for getting value from {@link #getStore()} and converts it by {@link #getConverter()}. + * But it will take time for getting value from {@link #getStore()} to set value. + * Do not use such strategy if object could be big (like Bitmap or long string). + * Do not use such strategy if object is mutable because multiple subscribers could then change it's state. + */ + CACHE_ACTUAL_VALUE, + /** + * Caching value so it won't spend time for getting value from {@link #getStore()} and converts it by {@link #getConverter()}. + * It won't take time or getting value from {@link #getStore()} to set value. + * Do not use such strategy if store object could be big (like byte-array of file). + * Do not use such strategy if object could be big (like Bitmap or long string). + * Do not use such strategy if object is mutable because multiple subscribers could then change it's state. + */ + CACHE_STORE_AND_ACTUAL_VALUE + + } + + /** + * Helper class to create various builders. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ + public static class BuilderCore { + + @NonNull + protected final TKey key; + @NonNull + protected final Type objectType; + @NonNull + private final Type storeObjectType; + @NonNull + private final Store store; + @NonNull + private final Converter converter; + @Nullable + private ObserveStrategy observeStrategy; + @Nullable + private Migration migration; + @Nullable + private TObject defaultValue; + @Nullable + private Scheduler storeScheduler; + private long cacheTimeMillis; + + protected BuilderCore(@NonNull final TKey key, + @NonNull final Type objectType, + @NonNull final Type storeObjectType, + @NonNull final Store store, + @NonNull final Converter converter) { + this(key, objectType, storeObjectType, store, converter, null, null, null, null, DEFAULT_CACHE_TIME_MILLIS); + } + + protected BuilderCore(@NonNull final BuilderCore sourceBuilder) { + this(sourceBuilder.key, sourceBuilder.objectType, sourceBuilder.storeObjectType, + sourceBuilder.store, sourceBuilder.converter, sourceBuilder.observeStrategy, + sourceBuilder.migration, sourceBuilder.defaultValue, sourceBuilder.storeScheduler, sourceBuilder.cacheTimeMillis); + } + + @SuppressWarnings({"PMD.ExcessiveParameterList", "CPD-START"}) + //CPD: it is same code as constructor of Storable + //ExcessiveParameterList: that's why we are using builder to create it + private BuilderCore(@NonNull final TKey key, + @NonNull final Type objectType, + @NonNull final Type storeObjectType, + @NonNull final Store store, + @NonNull final Converter converter, + @Nullable final ObserveStrategy observeStrategy, + @Nullable final Migration migration, + @Nullable final TObject defaultValue, + @Nullable final Scheduler storeScheduler, + final long cacheTimeMillis) { + this.key = key; + this.objectType = objectType; + this.storeObjectType = storeObjectType; + this.store = store; + this.converter = converter; + this.observeStrategy = observeStrategy; + this.migration = migration; + this.defaultValue = defaultValue; + this.storeScheduler = storeScheduler; + this.cacheTimeMillis = cacheTimeMillis; + } + + @SuppressWarnings("CPD-END") + protected void setStoreSchedulerInternal(@Nullable final Scheduler storeScheduler) { + this.storeScheduler = storeScheduler; + } + + protected void setObserveStrategyInternal(@Nullable final ObserveStrategy observeStrategy) { + this.observeStrategy = observeStrategy; + } + + protected void setMigrationInternal(@NonNull final Migration migration) { + this.migration = migration; + } + + protected void setCacheTimeInternal(final long cacheTime, @NonNull final TimeUnit timeUnit) { + this.cacheTimeMillis = timeUnit.toMillis(cacheTime); + } + + @Nullable + protected TObject getDefaultValue() { + return defaultValue; + } + + protected void setDefaultValueInternal(@NonNull final TObject defaultValue) { + this.defaultValue = defaultValue; + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Converter.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Converter.java new file mode 100644 index 0000000..5991932 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Converter.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Interface that is providing logic to convert value from specific type to type allowed to store in {@link Store} object and back. + * + * @param Type of original objects; + * @param Type of objects in store. + */ +public interface Converter { + + /** + * Converts specific object of objectType to object of storeObjectClass allowed to store. + * + * @param objectType Type of object; + * @param storeObjectType Type of store object allowed to store; + * @param object Object to be converted to store object; + * @return Object that is allowed to store into specific {@link Store}; + * @throws ConversionException Exception during conversion. Usually it indicates illegal state. + */ + @Nullable + TStoreObject toStoreObject(@NonNull Type objectType, @NonNull Type storeObjectType, @Nullable TObject object) + throws ConversionException; + + /** + * Converts specific store object of storeObjectClass to object of objectType. + * + * @param objectType Type of object; + * @param storeObjectType Type of store object allowed to store; + * @param storeObject Object from specific {@link Store}; + * @return Object converted from store object; + * @throws ConversionException Exception during conversion. Usually it indicates illegal state. + */ + @Nullable + TObject toObject(@NonNull Type objectType, @NonNull Type storeObjectType, @Nullable TStoreObject storeObject) + throws ConversionException; + + class ConversionException extends Exception { + + public ConversionException(@NonNull final String message) { + super(message); + } + + public ConversionException(@NonNull final String message, @NonNull final Throwable throwable) { + super(message, throwable); + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migration.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migration.java new file mode 100644 index 0000000..3fa2475 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migration.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; + +import java.util.Arrays; +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Single; + +/** + * Created by Gavriil Sitnikov on 06/10/2015. + * Object that allows to migrate some store objects from one version to another by migrators passed into constructor. + * Migrating objects should have same types of store key. + * + * @param Type of key of store objects. + */ +public class Migration { + + public static final long DEFAULT_VERSION = -1L; + + private final long latestVersion; + @NonNull + private final Store versionsStore; + @NonNull + private final List> migrators; + + @SafeVarargs + public Migration(@NonNull final Store versionsStore, + final long latestVersion, + @NonNull final Migrator... migrators) { + this.versionsStore = versionsStore; + this.latestVersion = latestVersion; + this.migrators = Arrays.asList(migrators); + } + + @NonNull + private Single loadCurrentVersion(@NonNull final TKey key) { + return versionsStore.loadObject(Long.class, key) + .map(version -> version.get() != null ? version.get() : DEFAULT_VERSION) + .onErrorResumeNext(throwable + -> Single.error(new MigrationException(String.format("Can't get version of '%s' from %s", key, versionsStore), throwable))); + } + + @NonNull + private Single makeMigrationChain(@NonNull final TKey key, @NonNull final VersionUpdater versionUpdater) { + Single chain = Single.fromCallable(() -> versionUpdater.initialVersion); + for (final Migrator migrator : migrators) { + chain = chain.flatMap(updatedVersion -> + migrator.canMigrate(key, updatedVersion) + .flatMap(canMigrate -> canMigrate + ? migrator.migrate(key, updatedVersion) + .doOnSuccess(newVersion + -> versionUpdater.updateVersion(newVersion, latestVersion, migrator)) + : Single.just(updatedVersion))); + } + return chain; + } + + /** + * Migrates some object by key to latest version. + * + * @param key Key of object to migrate. + */ + @NonNull + public Completable migrateToLatestVersion(@NonNull final TKey key) { + return loadCurrentVersion(key) + .flatMap(currentVersion -> { + final VersionUpdater versionUpdater = new VersionUpdater<>(key, versionsStore, currentVersion); + return makeMigrationChain(key, versionUpdater) + .doOnSuccess(lastUpdatedVersion -> { + if (lastUpdatedVersion < latestVersion) { + throw new NextLoopMigrationException(); + } + if (versionUpdater.initialVersion == versionUpdater.oldVersion) { + throw new MigrationException(String.format("Version of '%s' not updated from %s", + key, versionUpdater.initialVersion)); + } + }) + .retryWhen(attempts -> attempts + .switchMap(throwable -> throwable instanceof NextLoopMigrationException + ? Flowable.just(new Object()) : Flowable.error(throwable))); + }) + .toCompletable() + .andThen(versionsStore.storeObject(Long.class, key, latestVersion)) + .onErrorResumeNext(throwable -> { + if (throwable instanceof MigrationException) { + return Completable.error(throwable); + } + return Completable.error(new MigrationException(String.format("Can't migrate '%s'", key), throwable)); + }); + } + + private static class VersionUpdater { + + @NonNull + private final TKey key; + @NonNull + private final Store versionsStore; + private long oldVersion; + private long initialVersion; + + public VersionUpdater(@NonNull final TKey key, @NonNull final Store versionsStore, final long initialVersion) { + this.key = key; + this.versionsStore = versionsStore; + this.oldVersion = initialVersion; + this.initialVersion = initialVersion; + } + + public void updateVersion(final long updateVersion, final long latestVersion, @NonNull final Migrator migrator) { + if (initialVersion > updateVersion) { + throw new MigrationException(String.format("Version of '%s' downgraded from %s to %s [from %s by %s]", + key, initialVersion, updateVersion, versionsStore, migrator)); + } + if (updateVersion > latestVersion) { + throw new MigrationException( + String.format("Version of '%s' is %s and higher than latest version %s [from %s by %s]", + key, initialVersion, updateVersion, versionsStore, migrator)); + } + if (updateVersion == initialVersion) { + throw new MigrationException(String.format("Update version of '%s' equals current version '%s' [from %s by %s]", + key, updateVersion, versionsStore, migrator)); + } + oldVersion = initialVersion; + initialVersion = updateVersion; + } + + } + + private static class NextLoopMigrationException extends Exception { + } + + public static class MigrationException extends RuntimeException { + + public MigrationException(@NonNull final String message) { + super(message); + } + + public MigrationException(@NonNull final String message, @NonNull final Throwable throwable) { + super(message, throwable); + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migrator.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migrator.java new file mode 100644 index 0000000..41a3c53 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migrator.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; + +import io.reactivex.Single; + +/** + * Created by Gavriil Sitnikov on 05/10/2015. + * Abstract class of objects which are able to migrate some values from one version to another. + * Also it is able to move objects from one store to another. + * + * @param Type of keys of migrating values; + * @param Type of values from current store; + * @param Type of values from new store. Could be same as {@link TOldStoreObject}. + */ +public abstract class Migrator { + + @NonNull + private final Store oldStore; + @NonNull + private final Store newStore; + + public Migrator(@NonNull final Store oldStore, + @NonNull final Store newStore) { + this.oldStore = oldStore; + this.newStore = newStore; + } + + /** + * Returns if this migrator can migrate from specific version to some new version. + * + * @param version Version to migrate from; + * @return True if migrator supports migration from this version. + */ + public abstract boolean supportsMigrationFor(long version); + + /** + * Returns {@link Single} that emits if specific object with key of specific version could be migrated by this migrator. + * + * @param key Key of object to migrate; + * @param version Current version of object; + * @return {@link Single} that emits true if object with such key and version could be migrated. + */ + @NonNull + public Single canMigrate(@NonNull final TKey key, final long version) { + return supportsMigrationFor(version) ? oldStore.contains(key) : Single.just(false); + } + + /** + * Single that migrates object with specific key from some version to migrator's version. + * + * @param key Key of object to migrate; + * @param version Current version of object; + * @return {@link Single} that emits new version of object after migration process. + */ + @NonNull + public Single migrate(@NonNull final TKey key, final long version) { + return supportsMigrationFor(version) + ? migrateInternal(key, version, oldStore, newStore) + : Single.error(new Migration.MigrationException(String.format("Version %s of '%s' is not supported by %s", version, key, this))); + } + + /** + * Single that represents internal migration logic specified by implementation. + * + * @param key Key of object to migrate; + * @param version Current version of object; + * @param oldStore Old store of object; + * @param newStore new store of object; + * @return {@link Single} that emits new version of object after migration process. + */ + @NonNull + protected abstract Single migrateInternal(@NonNull TKey key, + long version, + @NonNull Store oldStore, + @NonNull Store newStore); + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/NonNullStorable.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/NonNullStorable.java new file mode 100644 index 0000000..3373ae6 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/NonNullStorable.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * {@link Storable} that should return not null value on get. + * If this rule is violated then it will throw {@link ShouldNotHappenException}. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ +public class NonNullStorable extends BaseStorable { + + public NonNullStorable(@NonNull final Builder builderCore) { + super(builderCore); + } + + @NonNull + @Override + public Observable observe() { + return observeOptionalValue() + .map(optional -> { + if (optional.get() == null) { + throw new ShouldNotHappenException(); + } + return optional.get(); + }); + } + + /** + * Created by Gavriil Sitnikov on 15/05/2016. + * Builder that is already contains not null default value. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ + @SuppressWarnings("CPD-START") + //CPD: it is same code as Builder of Storable because it's methods returning this and can't be inherited + public static class Builder extends BuilderCore { + + public Builder(@NonNull final Storable.Builder sourceBuilder, + @NonNull final TObject defaultValue) { + super(sourceBuilder); + if (defaultValue == null) { + throw new ShouldNotHappenException(); + } + setDefaultValueInternal(defaultValue); + } + + /** + * Sets specific {@link Scheduler} to store/load/convert values on it. + * + * @param storeScheduler Scheduler; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setStoreScheduler(@Nullable final Scheduler storeScheduler) { + setStoreSchedulerInternal(storeScheduler); + return this; + } + + /** + * Sets specific {@link ObserveStrategy} to cache value in memory in specific way. + * + * @param observeStrategy ObserveStrategy; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setObserveStrategy(@Nullable final ObserveStrategy observeStrategy) { + setObserveStrategyInternal(observeStrategy); + return this; + } + + /** + * Sets cache time for while value that cached by {@link #setObserveStrategy(ObserveStrategy)} + * will be in memory after everyone unsubscribe. + * It is important for example for cases when user switches between screens and hide/open app very fast. + * + * @param cacheTime Cache time value; + * @param timeUnit Cache time units. + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setCacheTime(final long cacheTime, @NonNull final TimeUnit timeUnit) { + setCacheTimeInternal(cacheTime, timeUnit); + return this; + } + + /** + * Sets specific {@link Migration} to migrate values from specific version to latest version. + * + * @param migration Migration; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setMigration(@NonNull final Migration migration) { + setMigrationInternal(migration); + return this; + } + + /** + * Building {@link NonNullStorable} object. + * + * @return New {@link NonNullStorable}. + */ + @NonNull + @SuppressWarnings("CPD-END") + public NonNullStorable build() { + if (getDefaultValue() == null) { + throw new ShouldNotHappenException(); + } + return new NonNullStorable<>(this); + } + + } +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/SameTypesConverter.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/SameTypesConverter.java new file mode 100644 index 0000000..9bfa228 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/SameTypesConverter.java @@ -0,0 +1,27 @@ +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; + +/** + * Simple safe converter that is doing nothing on conversion. + * + * @param Same type. + */ +public class SameTypesConverter implements Converter { + + @Nullable + @Override + public T toStoreObject(@NonNull final Type type1, @NonNull final Type type2, @Nullable final T object) { + return object; + } + + @Nullable + @Override + public T toObject(@NonNull final Type type1, @NonNull final Type type2, @Nullable final T object) { + return object; + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Storable.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Storable.java new file mode 100644 index 0000000..b891c0c --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Storable.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import ru.touchin.roboswag.core.utils.Optional; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Base class allows to async access to some store. + * Supports conversion between store and actual value. If it is not needed then use {@link SameTypesConverter} + * Supports migration from specific version to latest by {@link Migration} object. + * Allows to set default value which will be returned if actual value is null. + * Allows to declare specific {@link ObserveStrategy}. + * Also specific {@link Scheduler} could be specified to not create new scheduler per storable. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ +public class Storable extends BaseStorable> { + + public Storable(@NonNull final BuilderCore builderCore) { + super(builderCore); + } + + @NonNull + @Override + public Observable> observe() { + return observeOptionalValue(); + } + + /** + * Helper class to build {@link Storable}. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ + public static class Builder extends BuilderCore { + + public Builder(@NonNull final TKey key, + @NonNull final Type objectType, + @NonNull final Type storeObjectType, + @NonNull final Store store, + @NonNull final Converter converter) { + super(key, objectType, storeObjectType, store, converter); + } + + /** + * Sets specific {@link Scheduler} to store/load/convert values on it. + * + * @param storeScheduler Scheduler; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setStoreScheduler(@Nullable final Scheduler storeScheduler) { + setStoreSchedulerInternal(storeScheduler); + return this; + } + + /** + * Sets specific {@link ObserveStrategy} to cache value in memory in specific way. + * + * @param observeStrategy ObserveStrategy; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setObserveStrategy(@Nullable final ObserveStrategy observeStrategy) { + setObserveStrategyInternal(observeStrategy); + return this; + } + + /** + * Sets cache time for while value that cached by {@link #setObserveStrategy(ObserveStrategy)} will be in memory after everyone unsubscribe. + * It is important for example for cases when user switches between screens and hide/open app very fast. + * + * @param cacheTime Cache time value; + * @param timeUnit Cache time units. + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setCacheTime(final long cacheTime, @NonNull final TimeUnit timeUnit) { + setCacheTimeInternal(cacheTime, timeUnit); + return this; + } + + /** + * Sets specific {@link Migration} to migrate values from specific version to latest version. + * + * @param migration Migration; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setMigration(@NonNull final Migration migration) { + setMigrationInternal(migration); + return this; + } + + /** + * Sets value which will be returned instead of null. + * + * @param defaultValue Default value; + * @return Builder that allows to specify other fields. + */ + @NonNull + public NonNullStorable.Builder setDefaultValue(@NonNull final TObject defaultValue) { + return new NonNullStorable.Builder<>(this, defaultValue); + } + + /** + * Building {@link Storable} object. + * + * @return New {@link Storable}. + */ + @NonNull + public Storable build() { + return new Storable<>(this); + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Store.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Store.java new file mode 100644 index 0000000..c6c3a3b --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Store.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; + +import io.reactivex.Completable; +import io.reactivex.Single; +import ru.touchin.roboswag.core.utils.Optional; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Interface that is providing access to abstract object which can store (e.g. in file, database, remote store) + * some type of objects (e.g. String, byte[], Integer) by key. + * + * @param Type of keys for values; + * @param Type of values stored in store. + */ +public interface Store { + + /** + * Returns if store contains specific key related to some value. + * + * @param key Key which is finding in store; + * @return True if key have found in store. + */ + @NonNull + Single contains(@NonNull TKey key); + + /** + * Stores object to store with related key. + * + * @param storeObjectType Type of object to store; + * @param key Key related to object; + * @param storeObject Object to store; + */ + @NonNull + Completable storeObject(@NonNull Type storeObjectType, @NonNull TKey key, @Nullable TStoreObject storeObject); + + /** + * Loads object from store by key. + * + * @param storeObjectType Type of object to store; + * @param key Key related to object; + * @return Object from store found by key; + */ + @NonNull + Single> loadObject(@NonNull Type storeObjectType, @NonNull TKey key); + +} diff --git a/utils/.gitignore b/utils/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/utils/.gitignore @@ -0,0 +1 @@ +/build diff --git a/utils/build.gradle b/utils/build.gradle new file mode 100644 index 0000000..217c17b --- /dev/null +++ b/utils/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion 16 + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "com.android.support:support-annotations:$versions.supportLibrary" +} diff --git a/utils/src/main/AndroidManifest.xml b/utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6d76123 --- /dev/null +++ b/utils/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java b/utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java similarity index 96% rename from src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java rename to utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java index b6eb724..a80ee72 100644 --- a/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java @@ -38,23 +38,12 @@ import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; -import ru.touchin.roboswag.core.log.LcGroup; - /** * Created by Gavriil Sitnikov on 13/11/2015. * General utilities related to UI (Inflation, Views, Metrics, Activities etc.). */ public final class UiUtils { - /** - * Logging group to log UI metrics (like inflation or layout time etc.). - */ - public static final LcGroup UI_METRICS_LC_GROUP = new LcGroup("UI_METRICS"); - /** - * Logging group to log UI lifecycle (onCreate, onStart, onResume etc.). - */ - public static final LcGroup UI_LIFECYCLE_LC_GROUP = new LcGroup("UI_LIFECYCLE"); - /** * Method to inflate view with right layout parameters based on container and add inflated view as a child to it. * diff --git a/src/main/java/ru/touchin/roboswag/components/utils/destroyable/BaseDestroyable.kt b/utils/src/main/java/ru/touchin/roboswag/components/utils/destroyable/BaseDestroyable.kt similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/utils/destroyable/BaseDestroyable.kt rename to utils/src/main/java/ru/touchin/roboswag/components/utils/destroyable/BaseDestroyable.kt diff --git a/src/main/java/ru/touchin/roboswag/components/utils/destroyable/Destroyable.kt b/utils/src/main/java/ru/touchin/roboswag/components/utils/destroyable/Destroyable.kt similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/utils/destroyable/Destroyable.kt rename to utils/src/main/java/ru/touchin/roboswag/components/utils/destroyable/Destroyable.kt diff --git a/src/main/java/ru/touchin/roboswag/components/utils/spans/ColoredUrlSpan.java b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/ColoredUrlSpan.java similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/utils/spans/ColoredUrlSpan.java rename to utils/src/main/java/ru/touchin/roboswag/components/utils/spans/ColoredUrlSpan.java diff --git a/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java similarity index 93% rename from src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java rename to utils/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java index 5f84f91..b350912 100644 --- a/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java @@ -8,8 +8,6 @@ import android.text.TextPaint; import android.text.style.URLSpan; import android.view.View; -import ru.touchin.roboswag.core.log.Lc; - /** * Created by Gavriil Sitnikov on 14/11/2015. * Span that is opening phone call intent. @@ -28,7 +26,7 @@ public class PhoneSpan extends URLSpan { intent.setData(Uri.parse(getURL())); widget.getContext().startActivity(intent); } catch (final ActivityNotFoundException exception) { - Lc.assertion(exception); + // Do nothing } } diff --git a/src/main/java/ru/touchin/roboswag/components/utils/spans/TypefaceSpan.java b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/TypefaceSpan.java similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/utils/spans/TypefaceSpan.java rename to utils/src/main/java/ru/touchin/roboswag/components/utils/spans/TypefaceSpan.java diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/BiConsumer.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/BiConsumer.java new file mode 100644 index 0000000..c13ac22 --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/BiConsumer.java @@ -0,0 +1,19 @@ +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.Nullable; + +/** + * A functional interface (callback) that accepts two values (of possibly different types). + * @param the first value type + * @param the second value type + */ +public interface BiConsumer { + + /** + * Performs an operation on the given values. + * @param t1 the first value + * @param t2 the second value + * @throws Exception on error + */ + void accept(@Nullable T1 t1, @Nullable T2 t2) throws Exception; +} diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/ObjectUtils.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/ObjectUtils.java new file mode 100644 index 0000000..a8c2baf --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/ObjectUtils.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Some utilities related to objects. + */ +public final class ObjectUtils { + + /** + * Compares two objects if they are equals or not. If they are arrays then compare process same as {@link Arrays#deepEquals(Object[], Object[])}. + * + * @param object1 First object to compare; + * @param object2 Second object to compare; + * @return True if objects are equals. + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + //CompareObjectsWithEquals: we need to compare if it's same object + public static boolean equals(@Nullable final Object object1, @Nullable final Object object2) { + if (object1 == object2) { + return true; + } + if (object1 == null || object2 == null) { + return false; + } + + final Class elementType1 = object1.getClass().getComponentType(); + final Class elementType2 = object2.getClass().getComponentType(); + + if (!(elementType1 == null ? elementType2 == null : elementType1.equals(elementType2))) { + return false; + } + if (elementType1 == null) { + return object1.equals(object2); + } + return isArraysEquals(object1, object2, elementType1); + } + + /** + * Compares two collections if their elements are equals or not. + * + * @param collection1 First object to compare; + * @param collection2 Second object to compare; + * @return True if collections are equals. + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + //CompareObjectsWithEquals: we need to compare if it's same object + public static boolean isCollectionsEquals(@Nullable final Collection collection1, @Nullable final Collection collection2) { + if (collection1 == collection2) { + return true; + } + if (collection1 == null || collection2 == null) { + return false; + } + if (collection1.size() != collection2.size()) { + return false; + } + final Iterator collection2Iterator = collection2.iterator(); + for (final Object item1 : collection1) { + if (!equals(item1, collection2Iterator.next())) { + return false; + } + } + return true; + } + + /** + * Compares two maps if their elements are equals or not. + * + * @param map1 First object to compare; + * @param map2 Second object to compare; + * @return True if maps are equals. + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + //CompareObjectsWithEquals: we need to compare if it's same object + public static boolean isMapsEquals(@Nullable final Map map1, @Nullable final Map map2) { + return map1 == map2 || !(map1 == null || map2 == null) + && map1.size() == map2.size() + && map1.entrySet().containsAll(map2.entrySet()) + && map2.entrySet().containsAll(map1.entrySet()); + } + + @SuppressWarnings("PMD.AvoidUsingShortType") + private static boolean isArraysEquals(@NonNull final Object object1, @Nullable final Object object2, @NonNull final Class elementType) { + if (object1 instanceof Object[]) { + return Arrays.deepEquals((Object[]) object1, (Object[]) object2); + } else if (elementType == int.class) { + return Arrays.equals((int[]) object1, (int[]) object2); + } else if (elementType == char.class) { + return Arrays.equals((char[]) object1, (char[]) object2); + } else if (elementType == boolean.class) { + return Arrays.equals((boolean[]) object1, (boolean[]) object2); + } else if (elementType == byte.class) { + return Arrays.equals((byte[]) object1, (byte[]) object2); + } else if (elementType == long.class) { + return Arrays.equals((long[]) object1, (long[]) object2); + } else if (elementType == float.class) { + return Arrays.equals((float[]) object1, (float[]) object2); + } else if (elementType == double.class) { + return Arrays.equals((double[]) object1, (double[]) object2); + } else { + return Arrays.equals((short[]) object1, (short[]) object2); + } + } + + /** + * Calculates hashCode() of several objects. + * + * @param objects Objects to combine hashCode() of; + * @return Calculated hashCode(). + */ + public static int hashCode(@Nullable final Object... objects) { + return Arrays.hashCode(objects); + } + + /** + * Returns if class is simple like primitive, enum or string. + * + * @param objectClass Class to check if it's simple class; + * @return True if class is simple. + */ + public static boolean isSimpleClass(@NonNull final Class objectClass) { + return objectClass.isPrimitive() || objectClass.getSuperclass() == Number.class + || objectClass.isEnum() || objectClass == Boolean.class + || objectClass == String.class || objectClass == Object.class; + } + + /** + * Returns true if collection is null or empty. + * + * @param collection Collection to check; + * @return True if collection is null or empty. + */ + public static boolean isNullOrEmpty(@Nullable final Collection collection) { + return collection == null || collection.isEmpty(); + } + + /** + * Returns true if map is null or empty. + * + * @param map Map to check; + * @return True if map is null or empty. + */ + public static boolean isNullOrEmpty(@Nullable final Map map) { + return map == null || map.isEmpty(); + } + + private ObjectUtils() { + } + +} diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/Optional.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/Optional.java new file mode 100644 index 0000000..86a8493 --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/Optional.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.Nullable; + +import java.io.Serializable; + +/** + * Created by Gavriil Sitnikov on 16/04/2017. + * Holds nullable objects inside. It is needed to implement RxJava2 non-null emitting logic. + * + * @param Type of object. + */ +public class Optional implements Serializable { + + private static final long serialVersionUID = 1L; + + @Nullable + private final T value; + + public Optional(@Nullable final T value) { + this.value = value; + } + + /** + * Returns holding nullable object. + * + * @return Holding object. + */ + @Nullable + public T get() { + return value; + } + + @Override + public boolean equals(@Nullable final Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + final Optional that = (Optional) object; + return ObjectUtils.equals(value, that.value); + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + +} diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/ServiceBinder.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/ServiceBinder.java new file mode 100644 index 0000000..82f8acf --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/ServiceBinder.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.app.Service; +import android.os.Binder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Created by Gavriil Sitnikov on 03/10/2015. + * Basic binding to {@link Service} which holds service object inside. + */ +public class ServiceBinder extends Binder { + + @NonNull + private final TService service; + + public ServiceBinder(@NonNull final TService service) { + super(); + this.service = service; + } + + /** + * Returns service which created this binder. + * + * @return Returns service. + */ + @NonNull + public TService getService() { + return service; + } + + @Override + public boolean equals(@Nullable final Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + final ServiceBinder that = (ServiceBinder) object; + + return ObjectUtils.equals(service, that.service); + } + + @Override + public int hashCode() { + return service.hashCode(); + } + +} diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java new file mode 100644 index 0000000..639a5ab --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * 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. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.NonNull; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Created by Gavriil Sitnikov on 29/08/2016. + * Utility class to providing some string-related helper methods. + */ +public final class StringUtils { + + /** + * Returns MD5 of string. + * + * @param string String to get MD5 from; + * @return MD5 of string. + */ + @NonNull + public static String md5(@NonNull final String string) throws NoSuchAlgorithmException, UnsupportedEncodingException { + final MessageDigest digest = MessageDigest.getInstance("MD5"); + digest.update(string.getBytes("UTF-8")); + final byte[] messageDigestArray = digest.digest(); + + final StringBuilder hexString = new StringBuilder(); + for (final byte messageDigest : messageDigestArray) { + final String hex = Integer.toHexString(0xFF & messageDigest); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + private StringUtils() { + } + +} diff --git a/src/main/res/values/common_resources.xml b/utils/src/main/res/values/common_resources.xml similarity index 96% rename from src/main/res/values/common_resources.xml rename to utils/src/main/res/values/common_resources.xml index fff1084..f0221c8 100644 --- a/src/main/res/values/common_resources.xml +++ b/utils/src/main/res/values/common_resources.xml @@ -1,5 +1,5 @@ - + #0D000000 @@ -43,4 +43,4 @@ #E6FFFFFF #F3FFFFFF - \ No newline at end of file +