Components refactor

This commit is contained in:
Denis Karmyshakov 2018-08-06 01:39:16 +03:00
parent 1ffe50bdae
commit 3a990daa86
118 changed files with 3609 additions and 2811 deletions

36
.gitignore vendored
View File

@ -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

View File

@ -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'
]
}

13
gradle.properties Normal file
View File

@ -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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -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

172
gradlew vendored Executable file
View File

@ -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" "$@"

84
gradlew.bat vendored Normal file
View File

@ -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

1
logging/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

18
logging/build.gradle Normal file
View File

@ -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"
}

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.roboswag.core.log" />

View File

@ -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);
}
}
}

View File

@ -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<? extends Throwable>... exceptionsClassesToCut) {
new Handler(Looper.getMainLooper()).post(() -> {
final List<Throwable> 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() {
}
}

View File

@ -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<SimpleDateFormat> 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));
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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<T> extends ThreadLocal<T> {
@NonNull
private final Fabric<T> fabric;
public ThreadLocalValue(@NonNull final Fabric<T> fabric) {
super();
this.fabric = fabric;
}
@NonNull
@Override
protected T initialValue() {
return fabric.create();
}
/**
* Fabric of thread-local objects.
*
* @param <T> Type of objects.
*/
public interface Fabric<T> {
/**
* Creates object.
*
* @return new instance of object.
*/
@NonNull
T create();
}
}

1
navigation/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

23
navigation/build.gradle Normal file
View File

@ -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"
}

View File

@ -0,0 +1,3 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.touchin.roboswag.components.navigation"/>

View File

@ -36,4 +36,4 @@ public interface OnFragmentStartedListener {
*/
void onFragmentStarted(@NonNull Fragment fragment);
}
}

View File

@ -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();
}

View File

@ -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<TActivity extends FragmentActivity, TState e
if (inDebugMode) {
final long creationPeriod = SystemClock.elapsedRealtime() - creationTime;
if (creationPeriod > 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<TActivity extends FragmentActivity, TState e
@Nullable
@Override
public Animation onCreateAnimation(final int transit, final boolean enter, final int nextAnim) {
if (nextAnim == R.anim.fragment_slide_in_right_animation || nextAnim == R.anim.fragment_slide_out_right_animation) {
ViewCompat.setTranslationZ(getView(), 1F);
} else {
ViewCompat.setTranslationZ(getView(), 0F);
}
if (viewController != null) {
return viewController.onCreateAnimation(transit, enter, nextAnim);
} else {
@ -371,7 +364,7 @@ public class ViewControllerFragment<TActivity extends FragmentActivity, TState e
if (inDebugMode && lastMeasureTime > 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;
}

View File

@ -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.

View File

@ -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 <TActivity> Type of activity where such {@link ViewController} could be;
* @param <TState> Type of state;
* @param <TState> Type of state;
*/
public class ViewController<TActivity extends FragmentActivity, TState extends Parcelable> implements LifecycleOwner {
@ -221,7 +222,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
/**
* Returns a color state list associated with a particular resource ID.
*
* <p>
* <p>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<TActivity extends FragmentActivity, TState extends P
*/
@CallSuper
public void onCreate() {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
@ -285,10 +286,10 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
* {@link Animator} resources instead of {@link Animation} resources, {@code nextAnim}
* will be an animator resource.
*
* @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not
* set.
* @param enter {@code true} when the fragment is added/attached/shown or {@code false} when
* the fragment is removed/detached/hidden.
* @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not
* set.
* @param enter {@code true} when the fragment is added/attached/shown or {@code false} when
* the fragment is removed/detached/hidden.
* @param nextAnim The resource set in
* {@link FragmentTransaction#setCustomAnimations(int, int)},
* {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or
@ -306,10 +307,10 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
* {@link Animation} resources instead of {@link Animator} resources, {@code nextAnim}
* will be an animation resource.
*
* @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not
* set.
* @param enter {@code true} when the fragment is added/attached/shown or {@code false} when
* the fragment is removed/detached/hidden.
* @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not
* set.
* @param enter {@code true} when the fragment is added/attached/shown or {@code false} when
* the fragment is removed/detached/hidden.
* @param nextAnim The resource set in
* {@link FragmentTransaction#setCustomAnimations(int, int)},
* {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or
@ -335,7 +336,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
*/
@CallSuper
public void onStart() {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
UiUtils.OfViews.hideSoftInput(getContainer());
}
@ -345,7 +346,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
* Usually it is indicating that user can't see fragment on screen and useful to track analytics events.
*/
public void onAppear() {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
}
/**
@ -354,7 +355,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
*/
@CallSuper
public void onResume() {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
}
@ -373,7 +374,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
*/
@CallSuper
public void onPause() {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
}
@ -384,7 +385,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
*/
@CallSuper
public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
}
/**
@ -392,7 +393,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
* Usually it is indicating that user can't see fragment on screen and useful to track analytics events.
*/
public void onDisappear() {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
}
/**
@ -401,7 +402,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
*/
@CallSuper
public void onStop() {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
}
@ -411,7 +412,7 @@ public class ViewController<TActivity extends FragmentActivity, TState extends P
*/
@CallSuper
public void onDestroy() {
UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this));
LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this));
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}

1
sample/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

26
sample/build.gradle Normal file
View File

@ -0,0 +1,26 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 27
defaultConfig {
applicationId "ru.touchin.roboswag.components"
minSdkVersion 16
targetSdkVersion 27
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
}

21
sample/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ru.touchin.roboswag.components">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -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)
}
}

View File

@ -0,0 +1,35 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0"/>
<item
android:color="#00000000"
android:offset="1.0"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1"/>
</vector>

View File

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z"/>
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
</vector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Components</string>
</resources>

View File

@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

1
settings.gradle Normal file
View File

@ -0,0 +1 @@
include ':sample', ':utils', ':logging', ':navigation', ':storable'

View File

@ -1 +0,0 @@
<manifest package="ru.touchin.roboswag.components"/>

View File

@ -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 <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate.
*/
public abstract class AdapterDelegate<TViewHolder extends RecyclerView.ViewHolder> {
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<Object> 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<Object> 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<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> payloads
);
}

View File

@ -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<AdapterDelegate<*>>()
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<Any>) {
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")
}

View File

@ -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<TItem>(config: AsyncDifferConfig<TItem>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
constructor(diffCallback: DiffUtil.ItemCallback<TItem>) : this(AsyncDifferConfig.Builder<TItem>(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<Any>) {
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<TItem>) = differ.submitList(list)
/**
* Get the current List - any diffing to present this list has already been computed and
* dispatched via the ListUpdateCallback.
* <p>
* If a <code>null</code> List, or no List has been submitted, an empty list will be returned.
* <p>
* The returned list may not be mutated - mutations to content must be done through
* {@link #submitList(List)}.
*
* @return current List.
*/
fun getList(): List<TItem> = differ.currentList
fun getCollectionPosition(adapterPosition: Int) = adapterPosition - getHeadersCount()
}

View File

@ -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 <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate;
* @param <TItem> Type of items to bind to {@link RecyclerView.ViewHolder}s.
*/
public abstract class ItemAdapterDelegate<TViewHolder extends RecyclerView.ViewHolder, TItem> extends AdapterDelegate<TViewHolder> {
@Override
public boolean isForViewType(@NonNull final List<Object> 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<Object> 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<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> 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<Object> payloads
);
}

View File

@ -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)
}
}

View File

@ -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 <TViewHolder> Type of {@link RecyclerView.ViewHolder} of delegate.
*/
public abstract class PositionAdapterDelegate<TViewHolder extends RecyclerView.ViewHolder> extends AdapterDelegate<TViewHolder> {
@Override
public boolean isForViewType(@NonNull final List<Object> 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<Object> 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<Object> items,
final int adapterPosition,
final int collectionPosition,
@NonNull final List<Object> 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<Object> payloads) {
//do nothing by default
}
}

View File

@ -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 <T> Delegates.observable(
initialValue: T,
crossinline onChange: (newValue: T) -> Unit
): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(newValue)
}
inline fun <T> Delegates.distinctUntilChanged(
initialValue: T,
crossinline onChange: (newValue: T) -> Unit
): ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) =
if (newValue != null && oldValue != newValue) onChange(newValue) else Unit
}

View File

@ -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)
}
}

View File

@ -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 <T : View> 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)

View File

@ -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).
* <br><font color="yellow"> You require android.permission.BLUETOOTH and API level >= 11 if want to observe wireless headset state </font>
*/
public final class HeadsetStateObserver {
@NonNull
private final AudioManager audioManager;
@NonNull
private final Observable<Boolean> 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<Boolean> observeIsConnected() {
return connectedObservable;
}
private static class IsConnectedReceiver extends BroadcastReceiver {
@NonNull
private final BehaviorSubject<Boolean> isWiredConnectedChangedEvent;
@NonNull
private final BehaviorSubject<Boolean> 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;
}
}
}
}
}

View File

@ -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<Integer> volumeObservable;
@NonNull
private final PublishSubject<Integer> 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<Integer> observeVolume() {
return volumeObservable;
}
private static class VolumeObserver extends ContentObserver {
@NonNull
private final PublishSubject<Void> systemVolumeChangedEvent = PublishSubject.create();
public VolumeObserver() {
super(new Handler(Looper.getMainLooper()));
}
@Override
public void onChange(final boolean selfChange) {
super.onChange(selfChange);
systemVolumeChangedEvent.onNext(null);
}
}
}

View File

@ -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 <mario at quasimondo.com>
// http://incubator.quasimondo.com
// created Feburary 29, 2004
// Android port : Yahel Bouaziz <yahel at kayenko.com>
// 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 <mario@quasimondo.com>
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() {
}
}

View File

@ -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:
* <style name="MyAppLoadingBar">
* <item name="strokeWidth">3dp</item>
* <item name="color">@android:color/black</item>
* <item name="size">24dp</item>
* </style>
* <style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
* <item name="materialLoadingBarStyle">@style/MyAppLoadingBar</item>
* </style>
*/
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);
}
}

View File

@ -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;
}
}
}

View File

@ -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<String> 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<String> 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<String> 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);
}
}

View File

@ -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<String> 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<String> 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<String> 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;
}
}
}

View File

@ -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 <T> 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> 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<String> 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<String> 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<String> 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() {
}
}

View File

@ -1,6 +0,0 @@
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/fragmentTransitionTime"
android:fromXDelta="-50%p"
android:interpolator="@android:anim/accelerate_interpolator"
android:toXDelta="0"/>

View File

@ -1,6 +0,0 @@
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/fragmentTransitionTime"
android:fromXDelta="100%p"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="0"/>

View File

@ -1,6 +0,0 @@
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/fragmentTransitionTime"
android:fromXDelta="0"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="-50%p"/>

View File

@ -1,6 +0,0 @@
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/fragmentTransitionTime"
android:fromXDelta="0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toXDelta="100%p"/>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="0.0" android:toAlpha="1.0"
android:duration="@android:integer/config_mediumAnimTime" />

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1.0" android:toAlpha="0.0"
android:duration="@android:integer/config_mediumAnimTime" />

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="-100%p"
android:toXDelta="0"/>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="100%p"
android:toXDelta="0"/>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0"
android:toXDelta="-100%p"/>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<translate
xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0"
android:toXDelta="100%p"/>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/global_black_25">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@android:color/white"/>
</shape>
</item>
</ripple>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/global_white_25">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@android:color/white"/>
</shape>
</item>
</ripple>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/global_black_25" android:state_pressed="true"/>
</selector>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/global_white_25" android:state_pressed="true"/>
</selector>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="customTypeface" format="string"/>
<declare-styleable name="TypefacedTextView">
<attr name="lineStrategy" format="enum">
<enum name="singleLineEllipsize" value="0"/>
<enum name="singleLineMarquee" value="1"/>
<enum name="singleLineAutoScale" value="2"/>
<enum name="multilineEllipsize" value="3"/>
<enum name="multilineMarquee" value="4"/>
<enum name="singleLineEllipsizeMiddle" value="5"/>
<enum name="multilineLineEllipsizeMiddle" value="6"/>
</attr>
</declare-styleable>
<declare-styleable name="TypefacedEditText">
<attr name="isMultiline" format="boolean"/>
</declare-styleable>
<declare-styleable name="MaterialLoadingBar">
<attr name="strokeWidth" format="dimension"/>
<attr name="color" format="color"/>
<attr name="size" format="dimension"/>
<attr name="materialLoadingBarStyle" format="reference"/>
</declare-styleable>
</resources>

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="fragmentTransitionTime">250</integer>
</resources>

1
storable/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

24
storable/build.gradle Normal file
View File

@ -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"
}

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.core.observables.storable" />

View File

@ -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}.

View File

@ -45,9 +45,13 @@ public final class PreferenceUtils {
*/
@NonNull
public static Storable<String, String, String> stringStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) {
return new Storable.Builder<String, String, String>(name, String.class,
String.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.build();
return new Storable.Builder<String, String, String>(
name,
String.class,
String.class,
new PreferenceStore<String>(preferences),
new SameTypesConverter<>()
).build();
}
/**
@ -59,13 +63,18 @@ public final class PreferenceUtils {
* @return {@link Storable} for string.
*/
@NonNull
public static NonNullStorable<String, String, String> stringStorable(@NonNull final String name,
@NonNull final SharedPreferences preferences,
@NonNull final String defaultValue) {
return new Storable.Builder<String, String, String>(name, String.class,
String.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.setDefaultValue(defaultValue)
.build();
public static NonNullStorable<String, String, String> stringStorable(
@NonNull final String name,
@NonNull final SharedPreferences preferences,
@NonNull final String defaultValue
) {
return new Storable.Builder<String, String, String>(
name,
String.class,
String.class,
new PreferenceStore<String>(preferences),
new SameTypesConverter<>()
).setDefaultValue(defaultValue).build();
}
/**
@ -77,9 +86,13 @@ public final class PreferenceUtils {
*/
@NonNull
public static Storable<String, Long, Long> longStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) {
return new Storable.Builder<String, Long, Long>(name, Long.class,
Long.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.build();
return new Storable.Builder<String, Long, Long>(
name,
Long.class,
Long.class,
new PreferenceStore<Long>(preferences),
new SameTypesConverter<>()
).build();
}
/**
@ -91,13 +104,18 @@ public final class PreferenceUtils {
* @return {@link Storable} for long.
*/
@NonNull
public static NonNullStorable<String, Long, Long> longStorable(@NonNull final String name,
@NonNull final SharedPreferences preferences,
final long defaultValue) {
return new Storable.Builder<String, Long, Long>(name, Long.class,
Long.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.setDefaultValue(defaultValue)
.build();
public static NonNullStorable<String, Long, Long> longStorable(
@NonNull final String name,
@NonNull final SharedPreferences preferences,
final long defaultValue
) {
return new Storable.Builder<String, Long, Long>(
name,
Long.class,
Long.class,
new PreferenceStore<Long>(preferences),
new SameTypesConverter<>()
).setDefaultValue(defaultValue).build();
}
/**
@ -109,9 +127,13 @@ public final class PreferenceUtils {
*/
@NonNull
public static Storable<String, Boolean, Boolean> booleanStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) {
return new Storable.Builder<String, Boolean, Boolean>(name, Boolean.class,
Boolean.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.build();
return new Storable.Builder<String, Boolean, Boolean>(
name,
Boolean.class,
Boolean.class,
new PreferenceStore<Boolean>(preferences),
new SameTypesConverter<>()
).build();
}
/**
@ -123,13 +145,18 @@ public final class PreferenceUtils {
* @return {@link Storable} for boolean.
*/
@NonNull
public static NonNullStorable<String, Boolean, Boolean> booleanStorable(@NonNull final String name,
@NonNull final SharedPreferences preferences,
final boolean defaultValue) {
return new Storable.Builder<String, Boolean, Boolean>(name, Boolean.class,
Boolean.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.setDefaultValue(defaultValue)
.build();
public static NonNullStorable<String, Boolean, Boolean> booleanStorable(
@NonNull final String name,
@NonNull final SharedPreferences preferences,
final boolean defaultValue
) {
return new Storable.Builder<String, Boolean, Boolean>(
name,
Boolean.class,
Boolean.class,
new PreferenceStore<Boolean>(preferences),
new SameTypesConverter<>()
).setDefaultValue(defaultValue).build();
}
/**
@ -141,9 +168,13 @@ public final class PreferenceUtils {
*/
@NonNull
public static Storable<String, Integer, Integer> integerStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) {
return new Storable.Builder<String, Integer, Integer>(name, Integer.class,
Integer.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.build();
return new Storable.Builder<String, Integer, Integer>(
name,
Integer.class,
Integer.class,
new PreferenceStore<Integer>(preferences),
new SameTypesConverter<>()
).build();
}
/**
@ -155,13 +186,18 @@ public final class PreferenceUtils {
* @return {@link Storable} for integer.
*/
@NonNull
public static NonNullStorable<String, Integer, Integer> integerStorable(@NonNull final String name,
@NonNull final SharedPreferences preferences,
final int defaultValue) {
return new Storable.Builder<String, Integer, Integer>(name, Integer.class,
Integer.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.setDefaultValue(defaultValue)
.build();
public static NonNullStorable<String, Integer, Integer> integerStorable(
@NonNull final String name,
@NonNull final SharedPreferences preferences,
final int defaultValue
) {
return new Storable.Builder<String, Integer, Integer>(
name,
Integer.class,
Integer.class,
new PreferenceStore<Integer>(preferences),
new SameTypesConverter<>()
).setDefaultValue(defaultValue).build();
}
/**
@ -173,9 +209,13 @@ public final class PreferenceUtils {
*/
@NonNull
public static Storable<String, Float, Float> floatStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) {
return new Storable.Builder<String, Float, Float>(name, Float.class,
Float.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.build();
return new Storable.Builder<String, Float, Float>(
name,
Float.class,
Float.class,
new PreferenceStore<Float>(preferences),
new SameTypesConverter<>()
).build();
}
/**
@ -187,13 +227,18 @@ public final class PreferenceUtils {
* @return {@link Storable} for float.
*/
@NonNull
public static NonNullStorable<String, Float, Float> floatStorable(@NonNull final String name,
@NonNull final SharedPreferences preferences,
final float defaultValue) {
return new Storable.Builder<String, Float, Float>(name, Float.class,
Float.class, new PreferenceStore<>(preferences), new SameTypesConverter<>())
.setDefaultValue(defaultValue)
.build();
public static NonNullStorable<String, Float, Float> floatStorable(
@NonNull final String name,
@NonNull final SharedPreferences preferences,
final float defaultValue
) {
return new Storable.Builder<String, Float, Float>(
name,
Float.class,
Float.class,
new PreferenceStore<Float>(preferences),
new SameTypesConverter<>()
).setDefaultValue(defaultValue).build();
}
/**
@ -204,12 +249,18 @@ public final class PreferenceUtils {
* @return {@link Storable} for enum.
*/
@NonNull
public static <T extends Enum<T>> Storable<String, T, String> enumStorable(@NonNull final String name,
@NonNull final Class<T> enumClass,
@NonNull final SharedPreferences preferences) {
return new Storable.Builder<String, T, String>(name, enumClass,
String.class, new PreferenceStore<>(preferences), new EnumToStringConverter<>())
.build();
public static <T extends Enum<T>> Storable<String, T, String> enumStorable(
@NonNull final String name,
@NonNull final Class<T> enumClass,
@NonNull final SharedPreferences preferences
) {
return new Storable.Builder<String, T, String>(
name,
enumClass,
String.class,
new PreferenceStore<String>(preferences),
new EnumToStringConverter<>()
).build();
}
/**
@ -221,14 +272,19 @@ public final class PreferenceUtils {
* @return {@link Storable} for enum.
*/
@NonNull
public static <T extends Enum<T>> NonNullStorable<String, T, String> enumStorable(@NonNull final String name,
@NonNull final Class<T> enumClass,
@NonNull final SharedPreferences preferences,
@NonNull final T defaultValue) {
return new Storable.Builder<String, T, String>(name, enumClass,
String.class, new PreferenceStore<>(preferences), new EnumToStringConverter<>())
.setDefaultValue(defaultValue)
.build();
public static <T extends Enum<T>> NonNullStorable<String, T, String> enumStorable(
@NonNull final String name,
@NonNull final Class<T> enumClass,
@NonNull final SharedPreferences preferences,
@NonNull final T defaultValue
) {
return new Storable.Builder<String, T, String>(
name,
enumClass,
String.class,
new PreferenceStore<String>(preferences),
new EnumToStringConverter<>()
).setDefaultValue(defaultValue).build();
}
private PreferenceUtils() {

View File

@ -0,0 +1,299 @@
/*
Copyright (c) 2016-present, RxJava Contributors.
<p>
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
<p>
http://www.apache.org/licenses/LICENSE-2.0
<p>
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 <T> the value type
*/
@SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.AvoidUsingVolatile"})
//AvoidUsingVolatile: it's RxJava code
public final class ObservableRefCountWithCacheTime<T> extends Observable<T> implements HasUpstreamObservableSource<T> {
@NonNull
private final ConnectableObservable<? extends T> connectableSource;
@NonNull
private final ObservableSource<T> 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<T> source,
final long cacheTime, @NonNull final TimeUnit cacheTimeUnit) {
super();
this.connectableSource = source;
this.actualSource = source;
this.cacheTime = cacheTime;
this.cacheTimeUnit = cacheTimeUnit;
}
@NonNull
public ObservableSource<T> source() {
return actualSource;
}
private void cleanupWorker() {
if (worker != null) {
worker.dispose();
worker = null;
}
}
@Override
public void subscribeActual(@NonNull final Observer<? super T> 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<Disposable> onSubscribe(@NonNull final Observer<? super T> observer, @NonNull final AtomicBoolean writeLocked) {
return new DisposeConsumer(observer, writeLocked);
}
private void doSubscribe(@NonNull final Observer<? super T> 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<Disposable> implements Observer<T>, Disposable {
private static final long serialVersionUID = 3813126992133394324L;
@NonNull
private final Observer<? super T> subscriber;
@NonNull
private final CompositeDisposable currentBase;
@NonNull
private final Disposable resource;
public ConnectionObserver(@NonNull final Observer<? super T> 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<Disposable> {
@NonNull
private final Observer<? super T> observer;
@NonNull
private final AtomicBoolean writeLocked;
public DisposeConsumer(@NonNull final Observer<? super T> 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();
}
}
}
}

View File

@ -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 <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject};
* @param <TReturnObject> Type of actual value operating by Storable. Could be same as {@link TObject}.
*/
public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
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<TKey, TStoreObject> store;
@NonNull
private final Converter<TObject, TStoreObject> converter;
@NonNull
private final PublishSubject<Optional<TStoreObject>> newStoreValueEvent = PublishSubject.create();
@NonNull
private final Observable<Optional<TStoreObject>> storeValueObservable;
@NonNull
private final Observable<Optional<TObject>> valueObservable;
@NonNull
private final Scheduler scheduler;
public BaseStorable(@NonNull final BuilderCore<TKey, TObject, TStoreObject> 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<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter,
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> 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<TStoreObject> returnDefaultValueIfNull(@NonNull final Optional<TStoreObject> 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<Optional<TStoreObject>> createStoreInitialLoadingObservable(@Nullable final Migration<TKey> migration) {
final Single<Optional<TStoreObject>> 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<Optional<TStoreObject>> createStoreValueObservable(@NonNull final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> migration,
@Nullable final TObject defaultValue,
final long cacheTimeMillis) {
final Observable<Optional<TStoreObject>> storeInitialLoadingObservable = createStoreInitialLoadingObservable(migration);
final Observable<Optional<TStoreObject>> 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<Optional<TObject>> createValueObservable(@NonNull final Observable<Optional<TStoreObject>> storeValueObservable,
@NonNull final ObserveStrategy observeStrategy,
final long cacheTimeMillis) {
final Observable<Optional<TObject>> 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<TKey, TStoreObject> getStore() {
return store;
}
/**
* Returns {@link Converter} to convert values from store class to actual and back.
*
* @return Converter.
*/
@NonNull
public Converter<TObject, TStoreObject> 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<Optional<TObject>> 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<TReturnObject> 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<TReturnObject> 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 <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/
public static class BuilderCore<TKey, TObject, TStoreObject> {
@NonNull
protected final TKey key;
@NonNull
protected final Type objectType;
@NonNull
private final Type storeObjectType;
@NonNull
private final Store<TKey, TStoreObject> store;
@NonNull
private final Converter<TObject, TStoreObject> converter;
@Nullable
private ObserveStrategy observeStrategy;
@Nullable
private Migration<TKey> 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<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter) {
this(key, objectType, storeObjectType, store, converter, null, null, null, null, DEFAULT_CACHE_TIME_MILLIS);
}
protected BuilderCore(@NonNull final BuilderCore<TKey, TObject, TStoreObject> 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<TKey, TStoreObject> store,
@NonNull final Converter<TObject, TStoreObject> converter,
@Nullable final ObserveStrategy observeStrategy,
@Nullable final Migration<TKey> 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<TKey> 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;
}
}
}

View File

@ -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 <TObject> Type of original objects;
* @param <TStoreObject> Type of objects in store.
*/
public interface Converter<TObject, TStoreObject> {
/**
* 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);
}
}
}

View File

@ -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 <TKey> Type of key of store objects.
*/
public class Migration<TKey> {
public static final long DEFAULT_VERSION = -1L;
private final long latestVersion;
@NonNull
private final Store<TKey, Long> versionsStore;
@NonNull
private final List<Migrator<TKey, ?, ?>> migrators;
@SafeVarargs
public Migration(@NonNull final Store<TKey, Long> versionsStore,
final long latestVersion,
@NonNull final Migrator<TKey, ?, ?>... migrators) {
this.versionsStore = versionsStore;
this.latestVersion = latestVersion;
this.migrators = Arrays.asList(migrators);
}
@NonNull
private Single<Long> 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<Long> makeMigrationChain(@NonNull final TKey key, @NonNull final VersionUpdater versionUpdater) {
Single<Long> chain = Single.fromCallable(() -> versionUpdater.initialVersion);
for (final Migrator<TKey, ?, ?> 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<TKey> {
@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);
}
}
}

View File

@ -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 <TKey> Type of keys of migrating values;
* @param <TOldStoreObject> Type of values from current store;
* @param <TNewStoreObject> Type of values from new store. Could be same as {@link TOldStoreObject}.
*/
public abstract class Migrator<TKey, TOldStoreObject, TNewStoreObject> {
@NonNull
private final Store<TKey, TOldStoreObject> oldStore;
@NonNull
private final Store<TKey, TNewStoreObject> newStore;
public Migrator(@NonNull final Store<TKey, TOldStoreObject> oldStore,
@NonNull final Store<TKey, TNewStoreObject> 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<Boolean> 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<Long> 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<Long> migrateInternal(@NonNull TKey key,
long version,
@NonNull Store<TKey, TOldStoreObject> oldStore,
@NonNull Store<TKey, TNewStoreObject> newStore);
}

View File

@ -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 <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/
public class NonNullStorable<TKey, TObject, TStoreObject> extends BaseStorable<TKey, TObject, TStoreObject, TObject> {
public NonNullStorable(@NonNull final Builder<TKey, TObject, TStoreObject> builderCore) {
super(builderCore);
}
@NonNull
@Override
public Observable<TObject> 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 <TKey> Type of key to identify object;
* @param <TObject> Type of actual object;
* @param <TStoreObject> 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<TKey, TObject, TStoreObject> extends BuilderCore<TKey, TObject, TStoreObject> {
public Builder(@NonNull final Storable.Builder<TKey, TObject, TStoreObject> 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<TKey, TObject, TStoreObject> 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<TKey, TObject, TStoreObject> 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<TKey, TObject, TStoreObject> 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<TKey, TObject, TStoreObject> setMigration(@NonNull final Migration<TKey> migration) {
setMigrationInternal(migration);
return this;
}
/**
* Building {@link NonNullStorable} object.
*
* @return New {@link NonNullStorable}.
*/
@NonNull
@SuppressWarnings("CPD-END")
public NonNullStorable<TKey, TObject, TStoreObject> build() {
if (getDefaultValue() == null) {
throw new ShouldNotHappenException();
}
return new NonNullStorable<>(this);
}
}
}

Some files were not shown because too many files have changed in this diff Show More