diff --git a/.gitignore b/.gitignore index 63e899b..09b993d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,8 @@ -# Built application files -*.apk -*.ap_ - -# Files for the Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin/ -gen/ - -# Gradle files -.gradle/ -build/ -/*/build/ - -# Local configuration file (sdk path, etc) -local.properties - -# Log Files -*.log - -.gradle -.idea -.DS_Store -/captures *.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4aebbba --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "BuildScripts"] + path = BuildScripts + url = git@github.com:TouchInstinct/BuildScripts.git diff --git a/BuildScripts b/BuildScripts new file mode 160000 index 0000000..3fe4b2a --- /dev/null +++ b/BuildScripts @@ -0,0 +1 @@ +Subproject commit 3fe4b2a1aea149050b34c60ec7e2a876dfd8c0b2 diff --git a/api-logansquare/.gitignore b/api-logansquare/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/api-logansquare/.gitignore @@ -0,0 +1 @@ +/build diff --git a/api-logansquare/build.gradle b/api-logansquare/build.gradle new file mode 100644 index 0000000..3e774b1 --- /dev/null +++ b/api-logansquare/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":storable") + api 'net.danlew:android.joda:2.9.9.4' + + implementation "com.android.support:support-annotations:$versions.supportLibrary" + implementation "com.squareup.retrofit2:retrofit:$versions.retrofit" + implementation 'ru.touchin:logansquare:1.4.3' +} diff --git a/api-logansquare/src/main/AndroidManifest.xml b/api-logansquare/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d48ece2 --- /dev/null +++ b/api-logansquare/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/api-logansquare/src/main/java/com/bluelinelabs/logansquare/ConverterUtils.java b/api-logansquare/src/main/java/com/bluelinelabs/logansquare/ConverterUtils.java new file mode 100644 index 0000000..f8f7bba --- /dev/null +++ b/api-logansquare/src/main/java/com/bluelinelabs/logansquare/ConverterUtils.java @@ -0,0 +1,22 @@ +package com.bluelinelabs.logansquare; + +import android.support.annotation.NonNull; + +import java.lang.reflect.Type; + +/** + * Utility class for the {@link ru.touchin.templates.logansquare.LoganSquareJsonFactory}. This resides in LoganSquare's + * main package in order to take advantage of the package-visible ConcreteParameterizedType class, which is essential + * to the support of generic classes in the Retrofit converter. + */ +public final class ConverterUtils { + + @NonNull + public static ParameterizedType parameterizedTypeOf(@NonNull final Type type) { + return new ParameterizedType.ConcreteParameterizedType(type); + } + + private ConverterUtils() { + } + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java b/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java new file mode 100644 index 0000000..5755445 --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/ApiModel.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates; + +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; + +import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; + +/** + * Created by Gavriil Sitnikov on 11/08/2016. + * Just model from getting from API. + */ +public abstract class ApiModel implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Logging group to log API validation errors. + */ + public static final LcGroup API_VALIDATION_LC_GROUP = new LcGroup("API_VALIDATION"); + + /** + * Validates list of objects. Use it if objects in list extends {@link ApiModel}. + * + * @param collection Collection of items to check; + * @param collectionValidationRule Rule explaining what to do if invalid items found; + * @throws ValidationException Exception of validation. + */ + @SuppressWarnings({"PMD.PreserveStackTrace", "PMD.CyclomaticComplexity"}) + // PreserveStackTrace: it's ok - we are logging it on Lc.e() + public static void validateCollection(@NonNull final Collection collection, @NonNull final CollectionValidationRule collectionValidationRule) + throws ValidationException { + boolean haveValidItem = false; + int position = 0; + final Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + final Object item = iterator.next(); + if (!(item instanceof ApiModel)) { + if (item != null) { + // let's just think that all of items are not ApiModels + break; + } + continue; + } + + try { + ((ApiModel) item).validate(); + haveValidItem = true; + } catch (final ValidationException exception) { + switch (collectionValidationRule) { + case EXCEPTION_IF_ANY_INVALID: + throw exception; + case EXCEPTION_IF_ALL_INVALID: + iterator.remove(); + API_VALIDATION_LC_GROUP.e(exception, "Item %s is invalid at " + Lc.getCodePoint(null, 1), position); + if (!iterator.hasNext() && !haveValidItem) { + throw new ValidationException("Whole list is invalid at " + Lc.getCodePoint(null, 1)); + } + break; + case REMOVE_INVALID_ITEMS: + iterator.remove(); + API_VALIDATION_LC_GROUP.e(exception, "Item %s is invalid at " + Lc.getCodePoint(null, 1), position); + break; + default: + Lc.assertion("Unexpected rule " + collectionValidationRule); + break; + } + } + position++; + } + } + + /** + * Validates collection on emptiness. + * + * @param collection Collection to check; + * @throws ValidationException Exception of validation. + */ + protected static void validateCollectionNotEmpty(@NonNull final Collection collection) + throws ValidationException { + if (collection.isEmpty()) { + throw new ValidationException("List is empty at " + Lc.getCodePoint(null, 1)); + } + } + + /** + * Validates this object. Override it to write your own logic. + * + * @throws ValidationException Exception of validation. + */ + @CallSuper + public void validate() throws ValidationException { + //do nothing + } + + public enum CollectionValidationRule { + EXCEPTION_IF_ANY_INVALID, + EXCEPTION_IF_ALL_INVALID, + REMOVE_INVALID_ITEMS, + } + + /** + * Class of exceptions throws during {@link ApiModel} validation. + */ + public static class ValidationException extends IOException { + + public ValidationException(@NonNull final String reason) { + super(reason); + } + + } + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareBigDecimalConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareBigDecimalConverter.java new file mode 100644 index 0000000..9f08d6f --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareBigDecimalConverter.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.logansquare; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.bluelinelabs.logansquare.typeconverters.TypeConverter; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; + +import java.io.IOException; +import java.math.BigDecimal; + +import ru.touchin.roboswag.core.log.Lc; + +/** + * LoganSquare converter for java.math.BigDecimal + */ +@SuppressWarnings("CPD-START") // similar to LoganSquareJodaTimeConverter +public class LoganSquareBigDecimalConverter implements TypeConverter { + + @Nullable + @Override + public BigDecimal parse(@NonNull final JsonParser jsonParser) throws IOException { + final String dateString = jsonParser.getValueAsString(); + if (dateString == null) { + return null; + } + try { + return new BigDecimal(dateString); + } catch (final RuntimeException exception) { + Lc.assertion(exception); + } + return null; + } + + @Override + public void serialize(@Nullable final BigDecimal object, + @Nullable final String fieldName, + final boolean writeFieldNameForObject, + @NonNull final JsonGenerator jsonGenerator) + throws IOException { + if (fieldName != null) { + jsonGenerator.writeStringField(fieldName, object != null ? object.toString() : null); + } else { + jsonGenerator.writeString(object != null ? object.toString() : null); + } + } + +} + diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnum.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnum.java new file mode 100644 index 0000000..a5d0963 --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnum.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.logansquare; + +import android.support.annotation.NonNull; + +/** + * Created by Gavriil Sitnikov. + * LoganSquare enum base class. + */ +public interface LoganSquareEnum { + + @NonNull + String getValueName(); + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnumConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnumConverter.java new file mode 100644 index 0000000..c274667 --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareEnumConverter.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.logansquare; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter; + +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Gavriil Sitnikov. + * LoganSquare converter from String to Enum. + */ +@SuppressWarnings("PMD.UseVarargs") +public class LoganSquareEnumConverter extends StringBasedTypeConverter { + + @NonNull + private final T[] enumValues; + @Nullable + private final T defaultValue; + + public LoganSquareEnumConverter(@NonNull final T[] enumValues) { + this(enumValues, null); + } + + public LoganSquareEnumConverter(@NonNull final T[] enumValues, @Nullable final T defaultValue) { + super(); + this.enumValues = enumValues; + this.defaultValue = defaultValue; + } + + @Nullable + @Override + public T getFromString(@Nullable final String string) { + if (string == null) { + return defaultValue; + } + for (final T value : enumValues) { + if (value.getValueName().equals(string)) { + return value; + } + } + if (defaultValue != null) { + return defaultValue; + } + throw new ShouldNotHappenException("Enum parsing exception for value: " + string); + } + + @Nullable + @Override + public String convertToString(@Nullable final T object) { + if (object == null) { + return null; + } + return object.getValueName(); + } + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJodaTimeConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJodaTimeConverter.java new file mode 100644 index 0000000..11f52b8 --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJodaTimeConverter.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.logansquare; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.bluelinelabs.logansquare.typeconverters.TypeConverter; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; + +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormatter; + +import java.io.IOException; + +import ru.touchin.roboswag.core.log.Lc; + +/** + * LoganSquare converter for joda.time.DateTime + */ +public class LoganSquareJodaTimeConverter implements TypeConverter { + + @Nullable + private final DateTimeFormatter formatter; + + public LoganSquareJodaTimeConverter() { + this.formatter = null; + } + + public LoganSquareJodaTimeConverter(@Nullable final DateTimeFormatter formatter) { + this.formatter = formatter; + } + + @Nullable + @Override + public DateTime parse(@NonNull final JsonParser jsonParser) throws IOException { + final String dateString = jsonParser.getValueAsString(); + if (dateString == null || dateString.isEmpty()) { + return null; + } + try { + return DateTime.parse(dateString); + } catch (final RuntimeException exception) { + Lc.assertion(exception); + } + return null; + } + + @Override + public void serialize( + @Nullable final DateTime object, + @Nullable final String fieldName, + final boolean writeFieldNameForObject, + @NonNull final JsonGenerator jsonGenerator + ) throws IOException { + final String serializedValue = object != null ? object.toString(formatter) : null; + if (fieldName != null) { + jsonGenerator.writeStringField(fieldName, !TextUtils.isEmpty(serializedValue) ? serializedValue : null); + } else { + jsonGenerator.writeString(!TextUtils.isEmpty(serializedValue) ? serializedValue : null); + } + } + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonFactory.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonFactory.java new file mode 100644 index 0000000..20c5b15 --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonFactory.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.logansquare; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.bluelinelabs.logansquare.ConverterUtils; +import com.bluelinelabs.logansquare.LoganSquare; +import com.fasterxml.jackson.core.JsonGenerator; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import okhttp3.RequestBody; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; +import ru.touchin.templates.retrofit.JsonRequestBodyConverter; +import ru.touchin.templates.retrofit.JsonResponseBodyConverter; + +/** + * Created by Gavriil Sitnikov on 2/06/2016. + * LoganSquareConverter class to use with {@link Retrofit} to parse and generate models based on Logan Square library. + */ +public class LoganSquareJsonFactory extends Converter.Factory { + + @NonNull + @Override + public Converter responseBodyConverter(@NonNull final Type type, + @NonNull final Annotation[] annotations, + @NonNull final Retrofit retrofit) { + return new LoganSquareJsonResponseBodyConverter<>(type); + } + + @NonNull + @Override + public Converter requestBodyConverter(@NonNull final Type type, + @NonNull final Annotation[] parameterAnnotations, + @NonNull final Annotation[] methodAnnotations, + @NonNull final Retrofit retrofit) { + return new LoganSquareRequestBodyConverter<>(); + } + + @Nullable + @Override + public Converter stringConverter(@NonNull final Type type, @NonNull final Annotation[] annotations, @NonNull final Retrofit retrofit) { + if (type instanceof Class && ((Class) type).getSuperclass() == Enum.class) { + return new LoganSquareStringEnumConverter<>(); + } else { + return super.stringConverter(type, annotations, retrofit); + } + } + + public static class LoganSquareJsonResponseBodyConverter extends JsonResponseBodyConverter { + + @NonNull + private final Type type; + + public LoganSquareJsonResponseBodyConverter(@NonNull final Type type) { + super(); + this.type = type; + } + + @SuppressWarnings("unchecked") + @NonNull + @Override + protected T parseResponse(@NonNull final ResponseBody value) throws IOException { + if (type instanceof ParameterizedType) { + final ParameterizedType parameterizedType = (ParameterizedType) type; + final Type[] typeArguments = parameterizedType.getActualTypeArguments(); + final Type firstType = typeArguments[0]; + + final Type rawType = parameterizedType.getRawType(); + if (rawType == Map.class) { + return (T) LoganSquare.parseMap(value.byteStream(), (Class) typeArguments[1]); + } else if (rawType == List.class) { + return (T) LoganSquare.parseList(value.byteStream(), (Class) firstType); + } else { + // Generics + return (T) LoganSquare.parse(value.byteStream(), ConverterUtils.parameterizedTypeOf(type)); + } + } else { + return (T) LoganSquare.parse(value.byteStream(), (Class) type); + } + } + + } + + public static class LoganSquareRequestBodyConverter extends JsonRequestBodyConverter { + + @Override + protected void writeValueToByteArray(@NonNull final T value, @NonNull final ByteArrayOutputStream byteArrayOutputStream) throws IOException { + LoganSquare.serialize(value, byteArrayOutputStream); + } + + } + + public static class LoganSquareStringEnumConverter implements Converter { + + @Nullable + @SuppressWarnings({"unchecked", "TryFinallyCanBeTryWithResources"}) + @Override + public String convert(@NonNull final T value) throws IOException { + final StringWriter writer = new StringWriter(); + try { + final JsonGenerator generator = LoganSquare.JSON_FACTORY.createGenerator(writer); + LoganSquare.typeConverterFor((Class) value.getClass()).serialize(value, null, false, generator); + generator.close(); + return writer.toString().replaceAll("\"", ""); + } finally { + writer.close(); + } + } + + } + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonModel.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonModel.java new file mode 100644 index 0000000..ea67fbe --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquareJsonModel.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.logansquare; + +import android.support.annotation.Nullable; + +import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.templates.ApiModel; + +/** + * Created by Gavriil Sitnikov. + * Just model from getting from API via LoganSquare. + */ +public abstract class LoganSquareJsonModel extends ApiModel { + + /** + * Throws exception if object is missed or null. + * + * @param object Value of field to check; + * @throws ValidationException Exception of validation. + */ + protected static void validateNotNull(@Nullable final Object object) throws ValidationException { + if (object == null) { + throw new ValidationException("Not nullable object is null or missed at " + Lc.getCodePoint(null, 1)); + } + } + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquarePreferences.java b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquarePreferences.java new file mode 100644 index 0000000..763ccf0 --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/logansquare/LoganSquarePreferences.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.logansquare; + +import android.content.SharedPreferences; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.bluelinelabs.logansquare.LoganSquare; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; + +import ru.touchin.roboswag.components.utils.storables.PreferenceStore; +import ru.touchin.roboswag.core.observables.storable.Converter; +import ru.touchin.roboswag.core.observables.storable.NonNullStorable; +import ru.touchin.roboswag.core.observables.storable.Storable; +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Gavriil Sitnikov on 26/12/2016. + * Utility class to get {@link Storable} that is storing LoganSquare (Json) generated object into preferences. + */ +@SuppressWarnings("CPD-START") +//CPD: it is same code as in GoogleJsonPreferences +public final class LoganSquarePreferences { + + @NonNull + public static Storable jsonStorable(@NonNull final String name, + @NonNull final Class jsonClass, + @NonNull final SharedPreferences preferences) { + return new Storable.Builder(name, jsonClass, String.class, new PreferenceStore<>(preferences), new JsonConverter<>()) + .setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE) + .build(); + } + + @NonNull + public static NonNullStorable jsonStorable(@NonNull final String name, + @NonNull final Class jsonClass, + @NonNull final SharedPreferences preferences, + @NonNull final T defaultValue) { + return new Storable.Builder(name, jsonClass, String.class, new PreferenceStore<>(preferences), new JsonConverter<>()) + .setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE) + .setDefaultValue(defaultValue) + .build(); + } + + @NonNull + public static Storable, String> jsonListStorable(@NonNull final String name, + @NonNull final Class jsonListItemClass, + @NonNull final SharedPreferences preferences) { + return new Storable.Builder<>(name, List.class, String.class, new PreferenceStore<>(preferences), new JsonListConverter<>(jsonListItemClass)) + .setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE) + .build(); + } + + @NonNull + public static NonNullStorable, String> jsonListStorable(@NonNull final String name, + @NonNull final Class jsonListItemClass, + @NonNull final SharedPreferences preferences, + @NonNull final List defaultValue) { + return new Storable.Builder<>(name, List.class, String.class, new PreferenceStore<>(preferences), new JsonListConverter<>(jsonListItemClass)) + .setObserveStrategy(Storable.ObserveStrategy.CACHE_ACTUAL_VALUE) + .setDefaultValue(defaultValue) + .build(); + } + + private LoganSquarePreferences() { + } + + public static class JsonConverter implements Converter { + + @Nullable + @Override + public String toStoreObject(@NonNull final Type jsonObjectType, @NonNull final Type stringType, @Nullable final TJsonObject object) { + if (object == null) { + return null; + } + try { + return LoganSquare.serialize(object); + } catch (final IOException exception) { + throw new ShouldNotHappenException(exception); + } + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public TJsonObject toObject(@NonNull final Type jsonObjectClass, @NonNull final Type storeObjectType, @Nullable final String storeValue) { + if (storeValue == null) { + return null; + } + try { + return LoganSquare.parse(storeValue, (Class) jsonObjectClass); + } catch (final IOException exception) { + throw new ShouldNotHappenException(exception); + } + } + + } + + public static class JsonListConverter implements Converter, String> { + + @NonNull + private final Class itemClass; + + public JsonListConverter(@NonNull final Class itemClass) { + this.itemClass = itemClass; + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public String toStoreObject(@NonNull final Type jsonObjectType, @NonNull final Type stringType, @Nullable final List object) { + if (object == null) { + return null; + } + try { + return LoganSquare.serialize(object, itemClass); + } catch (final IOException exception) { + throw new ShouldNotHappenException(exception); + } + } + + @Nullable + @Override + public List toObject(@NonNull final Type jsonObjectType, @NonNull final Type stringType, @Nullable final String storeValue) { + if (storeValue == null) { + return null; + } + try { + return LoganSquare.parseList(storeValue, itemClass); + } catch (final IOException exception) { + throw new ShouldNotHappenException(exception); + } + } + + } + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonRequestBodyConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonRequestBodyConverter.java new file mode 100644 index 0000000..e0ea113 --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonRequestBodyConverter.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2017 Touch Instinct + * + * 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.templates.retrofit; + + +import android.support.annotation.NonNull; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import retrofit2.Converter; +import ru.touchin.templates.ApiModel; + +/** + * Created by Gavriil Sitnikov on 14/02/2017. + * Object to serialize bodies of remote requests for Retrofit. + * + * @param Type of body object. + */ +public abstract class JsonRequestBodyConverter implements Converter { + + private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8"); + + @NonNull + @Override + public RequestBody convert(@NonNull final T value) throws IOException { + if (value instanceof ApiModel) { + ((ApiModel) value).validate(); + } + final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + writeValueToByteArray(value, byteArrayOutputStream); + return RequestBody.create(MEDIA_TYPE, byteArrayOutputStream.toByteArray()); + } + + /** + * Serializing value to byte stream. + * + * @param value Value to serialize; + * @param byteArrayOutputStream Byte stream to write serialized bytes; + * @throws IOException Throws on serialization. + */ + protected abstract void writeValueToByteArray(@NonNull T value, @NonNull ByteArrayOutputStream byteArrayOutputStream) throws IOException; + +} diff --git a/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonResponseBodyConverter.java b/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonResponseBodyConverter.java new file mode 100644 index 0000000..614c8e2 --- /dev/null +++ b/api-logansquare/src/main/java/ru/touchin/templates/retrofit/JsonResponseBodyConverter.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2017 Touch Instinct + * + * 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.templates.retrofit; + +import android.support.annotation.NonNull; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.SocketException; +import java.util.Collection; +import java.util.Map; + +import javax.net.ssl.SSLException; + +import okhttp3.ResponseBody; +import okhttp3.internal.http2.StreamResetException; +import retrofit2.Converter; +import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.templates.ApiModel; + +/** + * Created by Gavriil Sitnikov on 14/02/2017. + * Object to deserialize responses of remote requests from Retrofit. + * + * @param Type of response object. + */ +public abstract class JsonResponseBodyConverter implements Converter { + + @SuppressWarnings("PMD.AvoidInstanceofChecksInCatchClause") + //AvoidInstanceofChecksInCatchClause: we just don't need assertion on specific exceptions + @NonNull + @Override + public T convert(@NonNull final ResponseBody value) throws IOException { + final T result; + try { + result = parseResponse(value); + } catch (final IOException exception) { + if (!(exception instanceof SocketException) + && !(exception instanceof InterruptedIOException) + && !(exception instanceof SSLException) + && !(exception instanceof StreamResetException)) { + Lc.assertion(exception); + } + throw exception; + } finally { + value.close(); + } + + if (result instanceof ApiModel) { + validateModel((ApiModel) result); + } + if (result instanceof Collection) { + validateCollection((Collection) result); + } + if (result instanceof Map) { + validateCollection(((Map) result).values()); + } + + return result; + } + + private void validateModel(@NonNull final ApiModel result) throws IOException { + try { + result.validate(); + } catch (final ApiModel.ValidationException validationException) { + Lc.assertion(validationException); + throw validationException; + } + } + + private void validateCollection(@NonNull final Collection result) throws IOException { + try { + ApiModel.validateCollection(result, getValidateCollectionRule()); + } catch (final ApiModel.ValidationException validationException) { + Lc.assertion(validationException); + throw validationException; + } + } + + @NonNull + protected ApiModel.CollectionValidationRule getValidateCollectionRule() { + return ApiModel.CollectionValidationRule.EXCEPTION_IF_ANY_INVALID; + } + + /** + * Parses response to specific object. + * + * @param value Response to parse; + * @return Parsed object; + * @throws IOException Throws during parsing. + */ + @NonNull + protected abstract T parseResponse(@NonNull ResponseBody value) throws IOException; + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 38e245d..a720f4f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,24 +1,39 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion compileSdk - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 +buildscript { + ext.kotlin_version = '1.2.61' + repositories { + google() + jcenter() } - - defaultConfig { - minSdkVersion 16 + dependencies { + classpath 'com.android.tools.build:gradle:3.1.4' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'de.aaschmid:gradle-cpd-plugin:1.0' } } -dependencies { - api project(':libraries:core') - - compileOnly "com.android.support:appcompat-v7:$supportLibraryVersion" - compileOnly "com.android.support:recyclerview-v7:$supportLibraryVersion" - - compileOnly "io.reactivex:rxandroid:$rxAndroidVersion" - compileOnly "io.reactivex:rxjava:$rxJavaVersion" +allprojects { + repositories { + google() + jcenter() + maven { url "http://dl.bintray.com/touchin/touchin-tools" } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +ext { + versions = [ + compileSdk : 27, + minSdk : 16, + 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', + crashlytics : '2.9.4' + ] } diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e30af81 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,13 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7a3265e Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..190eeaa --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Aug 05 23:37:20 MSK 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-extensions/.gitignore b/kotlin-extensions/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/kotlin-extensions/.gitignore @@ -0,0 +1 @@ +/build diff --git a/kotlin-extensions/build.gradle b/kotlin-extensions/build.gradle new file mode 100644 index 0000000..bcc6129 --- /dev/null +++ b/kotlin-extensions/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "com.android.support:recyclerview-v7:$versions.supportLibrary" +} diff --git a/kotlin-extensions/src/main/AndroidManifest.xml b/kotlin-extensions/src/main/AndroidManifest.xml new file mode 100644 index 0000000..25060ad --- /dev/null +++ b/kotlin-extensions/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt new file mode 100644 index 0000000..050bd6b --- /dev/null +++ b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/Delegates.kt @@ -0,0 +1,24 @@ +package ru.touchin.roboswag.components.extensions + +import kotlin.properties.Delegates +import kotlin.properties.ObservableProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * Simple observable delegate only for notification of new value. + */ +inline fun Delegates.observable( + initialValue: T, + crossinline onChange: (newValue: T) -> Unit +): ReadWriteProperty = object : ObservableProperty(initialValue) { + override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(newValue) +} + +inline fun Delegates.distinctUntilChanged( + initialValue: T, + crossinline onChange: (newValue: T) -> Unit +): ReadWriteProperty = object : ObservableProperty(initialValue) { + override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = + if (newValue != null && oldValue != newValue) onChange(newValue) else Unit +} diff --git a/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/View.kt b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/View.kt new file mode 100644 index 0000000..9f4ddca --- /dev/null +++ b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/View.kt @@ -0,0 +1,20 @@ +package ru.touchin.roboswag.components.extensions + +import android.os.Build +import android.view.View + +private const val RIPPLE_EFFECT_DELAY = 150L + +/** + * Sets click listener to view. On click it will call something after delay. + * + * @param delay Delay after which click listener will be called; + * @param listener Click listener. + */ +fun View.setOnRippleClickListener(delay: Long = RIPPLE_EFFECT_DELAY, listener: (View) -> Unit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setOnClickListener { view -> postDelayed({ if (hasWindowFocus()) listener(view) }, delay) } + } else { + setOnClickListener(listener) + } +} diff --git a/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt new file mode 100644 index 0000000..db7165f --- /dev/null +++ b/kotlin-extensions/src/main/java/ru/touchin/roboswag/components/extensions/ViewHolder.kt @@ -0,0 +1,32 @@ +package ru.touchin.roboswag.components.extensions + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.support.annotation.ColorInt +import android.support.annotation.ColorRes +import android.support.annotation.DrawableRes +import android.support.annotation.IdRes +import android.support.annotation.StringRes +import android.support.v4.content.ContextCompat +import android.support.v7.widget.RecyclerView +import android.view.View + +fun RecyclerView.ViewHolder.findViewById(@IdRes resId: Int): T = itemView.findViewById(resId) + +val RecyclerView.ViewHolder.context: Context + get() = itemView.context + +fun RecyclerView.ViewHolder.getText(@StringRes resId: Int): CharSequence = context.getText(resId) + +fun RecyclerView.ViewHolder.getString(@StringRes resId: Int): String = context.getString(resId) + +@SuppressWarnings("SpreadOperator") // it's OK for small arrays +fun RecyclerView.ViewHolder.getString(@StringRes resId: Int, vararg args: Any): String = context.getString(resId, *args) + +@ColorInt +fun RecyclerView.ViewHolder.getColor(@ColorRes resId: Int): Int = ContextCompat.getColor(context, resId) + +fun RecyclerView.ViewHolder.getColorStateList(@ColorRes resId: Int): ColorStateList? = ContextCompat.getColorStateList(context, resId) + +fun RecyclerView.ViewHolder.getDrawable(@DrawableRes resId: Int): Drawable? = ContextCompat.getDrawable(context, resId) diff --git a/lifecycle-common/.gitignore b/lifecycle-common/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/lifecycle-common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/lifecycle-common/build.gradle b/lifecycle-common/build.gradle new file mode 100644 index 0000000..e7ab339 --- /dev/null +++ b/lifecycle-common/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":navigation") + + compileOnly "javax.inject:javax.inject:1" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "com.android.support:appcompat-v7:$versions.supportLibrary" + + implementation "android.arch.lifecycle:extensions:$versions.lifecycle" +} diff --git a/lifecycle-common/src/main/AndroidManifest.xml b/lifecycle-common/src/main/AndroidManifest.xml new file mode 100644 index 0000000..26f2069 --- /dev/null +++ b/lifecycle-common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/lifecycle-common/src/main/java/ru/touchin/templates/livedata/SingleLiveEvent.kt b/lifecycle-common/src/main/java/ru/touchin/templates/livedata/SingleLiveEvent.kt new file mode 100644 index 0000000..27b0a1d --- /dev/null +++ b/lifecycle-common/src/main/java/ru/touchin/templates/livedata/SingleLiveEvent.kt @@ -0,0 +1,25 @@ +package ru.touchin.templates.livedata + +import android.arch.lifecycle.LifecycleOwner +import android.arch.lifecycle.MutableLiveData +import android.arch.lifecycle.Observer +import java.util.concurrent.atomic.AtomicBoolean + +class SingleLiveEvent : MutableLiveData() { + + private val pending = AtomicBoolean(false) + + override fun observe(owner: LifecycleOwner, observer: Observer) { + super.observe(owner, Observer { value -> + if (pending.compareAndSet(true, false)) { + observer.onChanged(value) + } + }) + } + + override fun setValue(value: T) { + pending.set(true) + super.setValue(value) + } + +} diff --git a/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/LifecycleViewModelProviders.kt b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/LifecycleViewModelProviders.kt new file mode 100644 index 0000000..41ee808 --- /dev/null +++ b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/LifecycleViewModelProviders.kt @@ -0,0 +1,49 @@ +package ru.touchin.templates.viewmodel + +import android.app.Activity +import android.arch.lifecycle.LifecycleOwner +import android.arch.lifecycle.ViewModelProvider +import android.arch.lifecycle.ViewModelProviders +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentActivity +import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController + +object LifecycleViewModelProviders { + + /** + * Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given + * {@code lifecycleOwner} is alive. More detailed explanation is in {@link ViewModel}. + *

+ * It uses the given {@link Factory} to instantiate new ViewModels. + * + * @param lifecycleOwner a lifecycle owner, in whose scope ViewModels should be retained (ViewController, Fragment, Activity) + * @param factory a {@code Factory} to instantiate new ViewModels + * @return a ViewModelProvider instance + */ + fun of(lifecycleOwner: LifecycleOwner, factory: ViewModelProvider.Factory = getViewModelFactory(lifecycleOwner)): ViewModelProvider = + when (lifecycleOwner) { + is ViewController<*, *> -> ViewModelProviders.of(lifecycleOwner.fragment, factory) + is Fragment -> ViewModelProviders.of(lifecycleOwner, factory) + is FragmentActivity -> ViewModelProviders.of(lifecycleOwner, factory) + else -> throw IllegalArgumentException("Not supported LifecycleOwner.") + } + + /** + * Returns ViewModelProvider.Factory instance from current lifecycleOwner. + * Search #ViewModelFactoryProvider are produced according to priorities: + * 1. View controller; + * 2. Fragment; + * 3. Parent fragment recursively; + * 4. Activity; + * 5. Application. + */ + fun getViewModelFactory(provider: Any): ViewModelProvider.Factory = + when (provider) { + is ViewModelFactoryProvider -> provider.viewModelFactory + is ViewController<*, *> -> getViewModelFactory(provider.fragment) + is Fragment -> getViewModelFactory(provider.parentFragment ?: provider.requireActivity()) + is Activity -> getViewModelFactory(provider.application) + else -> throw IllegalArgumentException("View model factory not found.") + } + +} diff --git a/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactory.kt b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactory.kt new file mode 100644 index 0000000..8061ba6 --- /dev/null +++ b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactory.kt @@ -0,0 +1,17 @@ +package ru.touchin.templates.viewmodel + +import android.arch.lifecycle.ViewModel +import android.arch.lifecycle.ViewModelProvider + +import javax.inject.Inject +import javax.inject.Provider + +class ViewModelFactory @Inject constructor( + private val creators: Map, @JvmSuppressWildcards Provider> +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T + = (creators[modelClass]?.get() as? T) ?: throw IllegalArgumentException("Unknown model class $modelClass") + +} diff --git a/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactoryProvider.kt b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactoryProvider.kt new file mode 100644 index 0000000..b1684e2 --- /dev/null +++ b/lifecycle-common/src/main/java/ru/touchin/templates/viewmodel/ViewModelFactoryProvider.kt @@ -0,0 +1,7 @@ +package ru.touchin.templates.viewmodel + +interface ViewModelFactoryProvider { + + val viewModelFactory: ViewModelFactory + +} diff --git a/lifecycle-rx/.gitignore b/lifecycle-rx/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/lifecycle-rx/.gitignore @@ -0,0 +1 @@ +/build diff --git a/lifecycle-rx/build.gradle b/lifecycle-rx/build.gradle new file mode 100644 index 0000000..0956030 --- /dev/null +++ b/lifecycle-rx/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } +} + +dependencies { + api project(":utils") + api project(":logging") + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "com.android.support:appcompat-v7:$versions.supportLibrary" + + implementation "android.arch.lifecycle:extensions:$versions.lifecycle" + + implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" + implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid" +} diff --git a/lifecycle-rx/src/main/AndroidManifest.xml b/lifecycle-rx/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9145647 --- /dev/null +++ b/lifecycle-rx/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/BaseDestroyable.kt b/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/BaseDestroyable.kt new file mode 100644 index 0000000..cca546b --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/BaseDestroyable.kt @@ -0,0 +1,65 @@ +package ru.touchin.livedata.destroyable + +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable + +/** + * Created by Oksana Pokrovskaya on 7/03/18. + * Simple implementation of [Destroyable]. Could be used to not implement interface but use such object inside. + */ +open class BaseDestroyable : Destroyable { + + private val subscriptions = CompositeDisposable() + + override fun clearSubscriptions() = subscriptions.clear() + + /** + * Call it on parent's onDestroy method. + */ + fun onDestroy() = subscriptions.dispose() + + override fun Flowable.untilDestroy( + onNext: (T) -> Unit, + onError: (Throwable) -> Unit, + onComplete: () -> Unit + ): Disposable = observeOn(AndroidSchedulers.mainThread()) + .subscribe(onNext, onError, onComplete) + .also { subscriptions.add(it) } + + override fun Observable.untilDestroy( + onNext: (T) -> Unit, + onError: (Throwable) -> Unit, + onComplete: () -> Unit + ): Disposable = observeOn(AndroidSchedulers.mainThread()) + .subscribe(onNext, onError, onComplete) + .also { subscriptions.add(it) } + + override fun Single.untilDestroy( + onSuccess: (T) -> Unit, + onError: (Throwable) -> Unit + ): Disposable = observeOn(AndroidSchedulers.mainThread()) + .subscribe(onSuccess, onError) + .also { subscriptions.add(it) } + + override fun Completable.untilDestroy( + onComplete: () -> Unit, + onError: (Throwable) -> Unit + ): Disposable = observeOn(AndroidSchedulers.mainThread()) + .subscribe(onComplete, onError) + .also { subscriptions.add(it) } + + override fun Maybe.untilDestroy( + onSuccess: (T) -> Unit, + onError: (Throwable) -> Unit, + onComplete: () -> Unit + ): Disposable = observeOn(AndroidSchedulers.mainThread()) + .subscribe(onSuccess, onError, onComplete) + .also { subscriptions.add(it) } + +} diff --git a/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/Destroyable.kt b/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/Destroyable.kt new file mode 100644 index 0000000..93dd9b8 --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/livedata/destroyable/Destroyable.kt @@ -0,0 +1,108 @@ +package ru.touchin.livedata.destroyable + +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import io.reactivex.internal.functions.Functions +import ru.touchin.roboswag.core.log.Lc +import ru.touchin.roboswag.core.utils.ShouldNotHappenException + +/** + * Created by Oksana Pokrovskaya on 7/03/18. + * Interface that should be implemented by ([android.arch.lifecycle.ViewModel] etc.) + * to not manually manage subscriptions. + * Use [.untilDestroy] method to subscribe to observable where you want and unsubscribe onDestroy. + */ +interface Destroyable { + + companion object { + private fun getActionThrowableForAssertion(codePoint: String, method: String = "untilDestroy"): (Throwable) -> Unit = { throwable -> + Lc.assertion(ShouldNotHappenException("Unexpected error on $method at $codePoint", throwable)) + } + } + + /** + * Removes all subscriptions + */ + fun clearSubscriptions() + + /** + * Method should be used to guarantee that observable won't be subscribed after onDestroy. + * It is automatically subscribing to the observable and calls onNextAction and onErrorAction on observable events. + * Don't forget to process errors if observable can emit them. + * + * @param onNext Action which will raise on every [io.reactivex.Emitter.onNext] item; + * @param onError Action which will raise on every [io.reactivex.Emitter.onError] throwable; + * @param onComplete Action which will raise on every [io.reactivex.Emitter.onComplete] item; + * @return [Disposable] which is wrapping source observable to unsubscribe from it onDestroy. + */ + fun Flowable.untilDestroy( + onNext: (T) -> Unit = Functions.emptyConsumer()::accept, + onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)), + onComplete: () -> Unit = Functions.EMPTY_ACTION::run + ): Disposable + + /** + * Method should be used to guarantee that observable won't be subscribed after onDestroy. + * It is automatically subscribing to the observable and calls onNextAction and onErrorAction on observable events. + * Don't forget to process errors if observable can emit them. + * + * @param onNext Action which will raise on every [io.reactivex.Emitter.onNext] item; + * @param onError Action which will raise on every [io.reactivex.Emitter.onError] throwable; + * @param onComplete Action which will raise on every [io.reactivex.Emitter.onComplete] item; + * @return [Disposable] which is wrapping source observable to unsubscribe from it onDestroy. + */ + fun Observable.untilDestroy( + onNext: (T) -> Unit = Functions.emptyConsumer()::accept, + onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)), + onComplete: () -> Unit = Functions.EMPTY_ACTION::run + ): Disposable + + /** + * Method should be used to guarantee that single won't be subscribed after onDestroy. + * It is automatically subscribing to the single and calls onSuccessAction and onErrorAction on single events. + * Don't forget to process errors if single can emit them. + * + * @param onSuccess Action which will raise on every [io.reactivex.SingleEmitter.onSuccess] item; + * @param onError Action which will raise on every [io.reactivex.SingleEmitter.onError] throwable; + * @return [Disposable] which is wrapping source single to unsubscribe from it onDestroy. + */ + fun Single.untilDestroy( + onSuccess: (T) -> Unit = Functions.emptyConsumer()::accept, + onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)) + ): Disposable + + /** + * Method should be used to guarantee that completable won't be subscribed after onDestroy. + * It is automatically subscribing to the completable and calls onCompletedAction and onErrorAction on completable events. + * Don't forget to process errors if completable can emit them. + * + * @param onComplete Action which will raise on every [io.reactivex.CompletableEmitter.onComplete] item; + * @param onError Action which will raise on every [io.reactivex.CompletableEmitter.onError] throwable; + * @return [Disposable] which is wrapping source completable to unsubscribe from it onDestroy. + */ + fun Completable.untilDestroy( + onComplete: () -> Unit = Functions.EMPTY_ACTION::run, + onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)) + ): Disposable + + /** + * Method should be used to guarantee that maybe won't be subscribed after onDestroy. + * It is automatically subscribing to the maybe and calls onSuccessAction and onErrorAction on maybe events. + * Don't forget to process errors if completable can emit them. + * + * @param onSuccess Action which will raise on every [io.reactivex.MaybeEmitter.onSuccess] ()} item; + * @param onError Action which will raise on every [io.reactivex.MaybeEmitter.onError] throwable; + * @param onComplete Action which will raise on every [io.reactivex.MaybeEmitter.onComplete] item; + * @return [Disposable] which is wrapping source maybe to unsubscribe from it onDestroy. + */ + fun Maybe.untilDestroy( + onSuccess: (T) -> Unit = Functions.emptyConsumer()::accept, + onError: (Throwable) -> Unit = getActionThrowableForAssertion(Lc.getCodePoint(this, 2)), + onComplete: () -> Unit = Functions.EMPTY_ACTION::run + ): Disposable + +} diff --git a/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/BaseLiveDataDispatcher.kt b/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/BaseLiveDataDispatcher.kt new file mode 100644 index 0000000..dc6f5f4 --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/BaseLiveDataDispatcher.kt @@ -0,0 +1,57 @@ +package ru.touchin.livedata.dispatcher + +import android.arch.lifecycle.MutableLiveData +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import ru.touchin.livedata.destroyable.BaseDestroyable +import ru.touchin.livedata.destroyable.Destroyable +import ru.touchin.templates.livedata.event.CompletableEvent +import ru.touchin.templates.livedata.event.MaybeEvent +import ru.touchin.templates.livedata.event.ObservableEvent +import ru.touchin.templates.livedata.event.SingleEvent + +class BaseLiveDataDispatcher(private val destroyable: BaseDestroyable = BaseDestroyable()) : LiveDataDispatcher, Destroyable by destroyable { + + override fun Flowable.dispatchTo(liveData: MutableLiveData>): Disposable { + liveData.value = ObservableEvent.Loading(liveData.value?.data) + return untilDestroy( + { data -> liveData.value = ObservableEvent.Success(data) }, + { throwable -> liveData.value = ObservableEvent.Error(throwable, liveData.value?.data) }, + { liveData.value = ObservableEvent.Completed(liveData.value?.data) }) + } + + override fun Observable.dispatchTo(liveData: MutableLiveData>): Disposable { + liveData.value = ObservableEvent.Loading(liveData.value?.data) + return untilDestroy( + { data -> liveData.value = ObservableEvent.Success(data) }, + { throwable -> liveData.value = ObservableEvent.Error(throwable, liveData.value?.data) }, + { liveData.value = ObservableEvent.Completed(liveData.value?.data) }) + } + + override fun Single.dispatchTo(liveData: MutableLiveData>): Disposable { + liveData.value = SingleEvent.Loading(liveData.value?.data) + return untilDestroy( + { data -> liveData.value = SingleEvent.Success(data) }, + { throwable -> liveData.value = SingleEvent.Error(throwable, liveData.value?.data) }) + } + + override fun Completable.dispatchTo(liveData: MutableLiveData): Disposable { + liveData.value = CompletableEvent.Loading + return untilDestroy( + { liveData.value = CompletableEvent.Completed }, + { throwable -> liveData.value = CompletableEvent.Error(throwable) }) + } + + override fun Maybe.dispatchTo(liveData: MutableLiveData>): Disposable { + liveData.value = MaybeEvent.Loading(liveData.value?.data) + return untilDestroy( + { data -> liveData.value = MaybeEvent.Success(data) }, + { throwable -> liveData.value = MaybeEvent.Error(throwable, liveData.value?.data) }, + { liveData.value = MaybeEvent.Completed(liveData.value?.data) }) + } + +} diff --git a/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/LiveDataDispatcher.kt b/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/LiveDataDispatcher.kt new file mode 100644 index 0000000..8ef3510 --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/livedata/dispatcher/LiveDataDispatcher.kt @@ -0,0 +1,27 @@ +package ru.touchin.livedata.dispatcher + +import android.arch.lifecycle.MutableLiveData +import io.reactivex.Completable +import io.reactivex.Flowable +import io.reactivex.Maybe +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.disposables.Disposable +import ru.touchin.templates.livedata.event.CompletableEvent +import ru.touchin.templates.livedata.event.MaybeEvent +import ru.touchin.templates.livedata.event.ObservableEvent +import ru.touchin.templates.livedata.event.SingleEvent + +interface LiveDataDispatcher { + + fun Flowable.dispatchTo(liveData: MutableLiveData>): Disposable + + fun Observable.dispatchTo(liveData: MutableLiveData>): Disposable + + fun Single.dispatchTo(liveData: MutableLiveData>): Disposable + + fun Completable.dispatchTo(liveData: MutableLiveData): Disposable + + fun Maybe.dispatchTo(liveData: MutableLiveData>): Disposable + +} diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/CompletableEvent.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/CompletableEvent.kt new file mode 100644 index 0000000..05fa013 --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/CompletableEvent.kt @@ -0,0 +1,14 @@ +package ru.touchin.templates.livedata.event + +/** + * Event class that emits from [io.reactivex.Completable]. + */ +sealed class CompletableEvent { + + object Loading: CompletableEvent() + + object Completed: CompletableEvent() + + data class Error(val throwable: Throwable): CompletableEvent() + +} diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/MaybeEvent.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/MaybeEvent.kt new file mode 100644 index 0000000..779fa5e --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/MaybeEvent.kt @@ -0,0 +1,16 @@ +package ru.touchin.templates.livedata.event + +/** + * Event class that emits from [io.reactivex.Maybe]. + */ +sealed class MaybeEvent(open val data: T?) { + + class Loading(data: T?): MaybeEvent(data) + + class Success(override val data: T): MaybeEvent(data) + + class Error(val throwable: Throwable, data: T?): MaybeEvent(data) + + class Completed(data: T?): MaybeEvent(data) + +} diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/ObservableEvent.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/ObservableEvent.kt new file mode 100644 index 0000000..02373d5 --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/ObservableEvent.kt @@ -0,0 +1,16 @@ +package ru.touchin.templates.livedata.event + +/** + * Event class that emits from [io.reactivex.Observable]. + */ +sealed class ObservableEvent(open val data: T?) { + + class Loading(data: T? = null): ObservableEvent(data) + + class Success(override val data: T): ObservableEvent(data) + + class Error(val throwable: Throwable, data: T? = null): ObservableEvent(data) + + class Completed(data: T? = null): ObservableEvent(data) + +} diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/SingleEvent.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/SingleEvent.kt new file mode 100644 index 0000000..f023393 --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/templates/livedata/event/SingleEvent.kt @@ -0,0 +1,14 @@ +package ru.touchin.templates.livedata.event + +/** + * Event class that emits from [io.reactivex.Single]. + */ +sealed class SingleEvent(open val data: T?) { + + class Loading(data: T?): SingleEvent(data) + + class Success(override val data: T): SingleEvent(data) + + class Error(val throwable: Throwable, data: T?): SingleEvent(data) + +} diff --git a/lifecycle-rx/src/main/java/ru/touchin/templates/viewmodel/RxViewModel.kt b/lifecycle-rx/src/main/java/ru/touchin/templates/viewmodel/RxViewModel.kt new file mode 100644 index 0000000..e27a860 --- /dev/null +++ b/lifecycle-rx/src/main/java/ru/touchin/templates/viewmodel/RxViewModel.kt @@ -0,0 +1,24 @@ +package ru.touchin.templates.viewmodel + +import android.arch.lifecycle.ViewModel +import android.support.annotation.CallSuper +import ru.touchin.livedata.dispatcher.BaseLiveDataDispatcher +import ru.touchin.livedata.dispatcher.LiveDataDispatcher +import ru.touchin.livedata.destroyable.BaseDestroyable +import ru.touchin.livedata.destroyable.Destroyable + +/** + * Base class of ViewModel with [io.reactivex.disposables.Disposable] handling. + */ +open class RxViewModel( + private val destroyable: BaseDestroyable = BaseDestroyable(), + private val liveDataDispatcher: BaseLiveDataDispatcher = BaseLiveDataDispatcher(destroyable) +) : ViewModel(), Destroyable by destroyable, LiveDataDispatcher by liveDataDispatcher { + + @CallSuper + override fun onCleared() { + super.onCleared() + destroyable.onDestroy() + } + +} diff --git a/lifecycle-rx/src/main/res/values/strings.xml b/lifecycle-rx/src/main/res/values/strings.xml new file mode 100644 index 0000000..b340b3d --- /dev/null +++ b/lifecycle-rx/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + lifecycle-rx + diff --git a/logging/.gitignore b/logging/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/logging/.gitignore @@ -0,0 +1 @@ +/build diff --git a/logging/build.gradle b/logging/build.gradle new file mode 100644 index 0000000..69333c1 --- /dev/null +++ b/logging/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "com.android.support:support-annotations:$versions.supportLibrary" +} diff --git a/logging/src/main/AndroidManifest.xml b/logging/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8ce26b3 --- /dev/null +++ b/logging/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java b/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java new file mode 100644 index 0000000..0c05b84 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/ConsoleLogProcessor.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * Simple {@link LogProcessor} implementation which is logging messages to console (logcat). + */ +public class ConsoleLogProcessor extends LogProcessor { + + private static final int MAX_LOG_LENGTH = 4000; + + public ConsoleLogProcessor(@NonNull final LcLevel lclevel) { + super(lclevel); + } + + @NonNull + private String normalize(@NonNull final String message) { + return message.replace("\r\n", "\n").replace("\0", ""); + } + + @Override + @SuppressWarnings({"WrongConstant", "LogConditional"}) + //WrongConstant, LogConditional: level.getPriority() is not wrong constant! + public void processLogMessage(@NonNull final LcGroup group, @NonNull final LcLevel level, + @NonNull final String tag, @NonNull final String message, @Nullable final Throwable throwable) { + final String messageToLog = normalize(message + (throwable != null ? '\n' + Log.getStackTraceString(throwable) : "")); + final int length = messageToLog.length(); + for (int i = 0; i < length; i++) { + int newline = messageToLog.indexOf('\n', i); + newline = newline != -1 ? newline : length; + do { + final int end = Math.min(newline, i + MAX_LOG_LENGTH); + Log.println(level.getPriority(), tag, messageToLog.substring(i, end)); + i = end; + } + while (i < newline); + } + + } + +} \ No newline at end of file diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java b/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java new file mode 100644 index 0000000..3d46668 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/Lc.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * General logging utility of RoboSwag library. + * You can initialize {@link LogProcessor} to intercept log messages and make decision how to show them. + * Also you can specify assertions behavior to manually make application more stable in production but intercept illegal states in some + * third-party tool to fix them later but not crash in production. + */ +@SuppressWarnings({"checkstyle:methodname", "PMD.ShortMethodName", "PMD.ShortClassName"}) +//MethodNameCheck,ShortMethodName: log methods better be 1-symbol +public final class Lc { + + public static final LcGroup GENERAL_LC_GROUP = new LcGroup("GENERAL"); + + public static final int STACK_TRACE_CODE_DEPTH; + + private static boolean crashOnAssertions = true; + @NonNull + private static LogProcessor logProcessor = new ConsoleLogProcessor(LcLevel.ERROR); + + static { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + int stackDepth; + for (stackDepth = 0; stackDepth < stackTrace.length; stackDepth++) { + if (stackTrace[stackDepth].getClassName().equals(Lc.class.getName())) { + break; + } + } + STACK_TRACE_CODE_DEPTH = stackDepth + 1; + } + + /** + * Flag to crash application or pass it to {@link LogProcessor#processLogMessage(LcGroup, LcLevel, String, String, Throwable)} + * on specific {@link LcGroup#assertion(Throwable)} points of code. + * + * @return True if application should crash on assertion. + */ + public static boolean isCrashOnAssertions() { + return crashOnAssertions; + } + + /** + * Returns {@link LogProcessor} object to intercept incoming log messages (by default it returns {@link ConsoleLogProcessor}). + * + * @return Specific {@link LogProcessor}. + */ + @NonNull + public static LogProcessor getLogProcessor() { + return logProcessor; + } + + /** + * Initialize general logging behavior. + * + * @param logProcessor {@link LogProcessor} to intercept all log messages; + * @param crashOnAssertions Flag to crash application + * or pass it to {@link LogProcessor#processLogMessage(LcGroup, LcLevel, String, String, Throwable)} + * on specific {@link LcGroup#assertion(Throwable)} points of code. + */ + public static void initialize(@NonNull final LogProcessor logProcessor, final boolean crashOnAssertions) { + Lc.crashOnAssertions = crashOnAssertions; + Lc.logProcessor = logProcessor; + } + + /** + * Logs debug message via {@link #GENERAL_LC_GROUP}. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void d(@NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.d(message, args); + } + + /** + * Logs debug message via {@link #GENERAL_LC_GROUP}. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void d(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.d(throwable, message, args); + } + + /** + * Logs info message via {@link #GENERAL_LC_GROUP}. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void i(@NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.i(message, args); + } + + /** + * Logs info message via {@link #GENERAL_LC_GROUP}. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void i(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.i(throwable, message, args); + } + + /** + * Logs warning message via {@link #GENERAL_LC_GROUP}. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void w(@NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.w(message, args); + } + + /** + * Logs warning message via {@link #GENERAL_LC_GROUP}. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void w(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.w(throwable, message, args); + } + + /** + * Logs error message via {@link #GENERAL_LC_GROUP}. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void e(@NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.e(message, args); + } + + /** + * Logs error message via {@link #GENERAL_LC_GROUP}. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public static void e(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + GENERAL_LC_GROUP.e(throwable, message, args); + } + + /** + * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app. + * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}. + * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}. + * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build. + * + * @param message Message that is describing assertion. + */ + public static void assertion(@NonNull final String message) { + GENERAL_LC_GROUP.assertion(message); + } + + /** + * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app. + * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}. + * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}. + * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build. + * + * @param throwable Exception that is describing assertion. + */ + public static void assertion(@NonNull final Throwable throwable) { + GENERAL_LC_GROUP.assertion(throwable); + } + + /** + * Throws assertion on main thread (to avoid Rx exceptions e.g.) and cuts top causes by type of exception class. + * + * @param assertion Source throwable; + * @param exceptionsClassesToCut Classes which will be cut from top of causes stack of source throwable. + */ + @SafeVarargs + public static void cutAssertion(@NonNull final Throwable assertion, @NonNull final Class... exceptionsClassesToCut) { + new Handler(Looper.getMainLooper()).post(() -> { + final List processedExceptions = new ArrayList<>(); + Throwable result = assertion; + boolean exceptionAssignableFromIgnores; + do { + exceptionAssignableFromIgnores = false; + processedExceptions.add(result); + for (final Class exceptionClass : exceptionsClassesToCut) { + if (result.getClass().isAssignableFrom(exceptionClass)) { + exceptionAssignableFromIgnores = true; + result = result.getCause(); + break; + } + } + } + while (exceptionAssignableFromIgnores && result != null && !processedExceptions.contains(result)); + Lc.assertion(result != null ? result : assertion); + }); + } + + /** + * Returns line of code from where this method called. + * + * @param caller Object who is calling for code point; + * @return String represents code point. + */ + @NonNull + public static String getCodePoint(@Nullable final Object caller) { + return getCodePoint(caller, 1); + } + + /** + * Returns line of code from where this method called. + * + * @param caller Object who is calling for code point; + * @param stackShift caller Shift of stack (e.g. 2 means two elements deeper); + * @return String represents code point. + */ + @NonNull + public static String getCodePoint(@Nullable final Object caller, final int stackShift) { + final StackTraceElement traceElement = Thread.currentThread().getStackTrace()[STACK_TRACE_CODE_DEPTH + stackShift]; + return traceElement.getMethodName() + '(' + traceElement.getFileName() + ':' + traceElement.getLineNumber() + ')' + + (caller != null ? " of object " + caller.getClass().getSimpleName() + '(' + Integer.toHexString(caller.hashCode()) + ')' : ""); + } + + /** + * Prints stacktrace in log with specified tag. + * + * @param tag Tag to be shown in logs. + */ + + @SuppressLint("LogConditional") + public static void printStackTrace(@NonNull final String tag) { + final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + if (Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, TextUtils.join("\n", Arrays.copyOfRange(stackTrace, STACK_TRACE_CODE_DEPTH, stackTrace.length))); + } + } + + private Lc() { + } + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java new file mode 100644 index 0000000..3ccf1fa --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LcGroup.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.text.SimpleDateFormat; +import java.util.Locale; + +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; +import ru.touchin.roboswag.core.utils.ThreadLocalValue; + +/** + * Created by Gavriil Sitnikov on 14/05/2016. + * Group of log messages with specific tag prefix (name of group). + * It could be used in specific {@link LogProcessor} to filter messages by group. + */ +@SuppressWarnings({"checkstyle:methodname", "PMD.ShortMethodName"}) +//MethodNameCheck,ShortMethodName: log methods better be 1-symbol +public class LcGroup { + + /** + * Logging group to log UI metrics (like inflation or layout time etc.). + */ + public static final LcGroup UI_METRICS = new LcGroup("UI_METRICS"); + /** + * Logging group to log UI lifecycle (onCreate, onStart, onResume etc.). + */ + public static final LcGroup UI_LIFECYCLE = new LcGroup("UI_LIFECYCLE"); + + private static final ThreadLocalValue DATE_TIME_FORMATTER + = new ThreadLocalValue<>(() -> new SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault())); + + @NonNull + private final String name; + private boolean disabled; + + public LcGroup(@NonNull final String name) { + this.name = name; + } + + /** + * Disables logging of this group. + */ + public void disable() { + disabled = true; + } + + /** + * Enables logging of this group. + */ + public void enable() { + disabled = false; + } + + @NonNull + private String createLogTag() { + final StackTraceElement trace = Thread.currentThread().getStackTrace()[Lc.STACK_TRACE_CODE_DEPTH + 3]; + return trace.getFileName() + ':' + trace.getLineNumber(); + } + + @SuppressWarnings("PMD.AvoidCatchingThrowable") + //AvoidCatchingThrowable: it is needed to safety format message + @Nullable + private String createFormattedMessage(@Nullable final String message, @NonNull final Object... args) { + try { + if (args.length > 0 && message == null) { + throw new ShouldNotHappenException("Args are not empty but format message is null"); + } + return message != null ? (args.length > 0 ? String.format(message, args) : message) : null; + } catch (final Throwable formattingException) { + Lc.assertion(formattingException); + return null; + } + } + + @NonNull + private String createLogMessage(@Nullable final String formattedMessage) { + return DATE_TIME_FORMATTER.get().format(System.currentTimeMillis()) + + ' ' + Thread.currentThread().getName() + + ' ' + name + + (formattedMessage != null ? (' ' + formattedMessage) : ""); + } + + private void logMessage(@NonNull final LcLevel logLevel, @Nullable final String message, + @Nullable final Throwable throwable, @NonNull final Object... args) { + if (disabled || logLevel.lessThan(Lc.getLogProcessor().getMinLogLevel())) { + return; + } + + if (throwable == null && args.length > 0 && args[0] instanceof Throwable) { + Lc.w("Maybe you've misplaced exception with first format arg? format: %s; arg: %s", message, args[0]); + } + + final String formattedMessage = createFormattedMessage(message, args); + if (logLevel == LcLevel.ASSERT && Lc.isCrashOnAssertions()) { + throw createAssertion(formattedMessage, throwable); + } + + Lc.getLogProcessor().processLogMessage(this, logLevel, createLogTag(), createLogMessage(formattedMessage), throwable); + } + + @NonNull + private ShouldNotHappenException createAssertion(@Nullable final String message, @Nullable final Throwable exception) { + return exception != null + ? (message != null ? new ShouldNotHappenException(message, exception) + : (exception instanceof ShouldNotHappenException ? (ShouldNotHappenException) exception : new ShouldNotHappenException(exception))) + : (message != null ? new ShouldNotHappenException(message) : new ShouldNotHappenException()); + } + + /** + * Logs debug message. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void d(@NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.DEBUG, message, null, args); + } + + /** + * Logs debug message. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void d(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.DEBUG, message, throwable, args); + } + + /** + * Logs info message. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void i(@NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.INFO, message, null, args); + } + + /** + * Logs info message. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void i(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.INFO, message, throwable, args); + } + + /** + * Logs warning message. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void w(@NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.WARN, message, null, args); + } + + /** + * Logs warning message. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void w(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.WARN, message, throwable, args); + } + + /** + * Logs error message. + * + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void e(@NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.ERROR, message, null, args); + } + + /** + * Logs error message. + * + * @param throwable Exception to log; + * @param message Message or format of message to log; + * @param args Arguments of formatted message. + */ + public void e(@NonNull final Throwable throwable, @NonNull final String message, @NonNull final Object... args) { + logMessage(LcLevel.ERROR, message, throwable, args); + } + + /** + * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app. + * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}. + * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}. + * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build. + * + * @param message Message that is describing assertion. + */ + public void assertion(@NonNull final String message) { + logMessage(LcLevel.ASSERT, "Assertion appears at %s with message: %s", null, Lc.getCodePoint(null, 2), message); + } + + /** + * Processes assertion. Normally it will throw {@link ShouldNotHappenException} and crash app. + * If it should crash or not is specified at {@link Lc#isCrashOnAssertions()}. + * In some cases crash on assertions should be switched off and assertion should be processed in {@link LogProcessor}. + * It is useful for example to not crash but log it as handled crash in Crashlitycs in production build. + * + * @param throwable Exception that is describing assertion. + */ + public void assertion(@NonNull final Throwable throwable) { + logMessage(LcLevel.ASSERT, "Assertion appears at %s", throwable, Lc.getCodePoint(null, 2)); + } + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java new file mode 100644 index 0000000..9dbd5e5 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LcLevel.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.support.annotation.NonNull; +import android.util.Log; + +/** + * Created by Gavriil Sitnikov on 14/05/2016. + * Level of log message. + */ +public enum LcLevel { + + VERBOSE(Log.VERBOSE), + DEBUG(Log.DEBUG), + INFO(Log.INFO), + WARN(Log.WARN), + ERROR(Log.ERROR), + ASSERT(Log.ASSERT); + + private final int priority; + + LcLevel(final int priority) { + this.priority = priority; + } + + /** + * Standard {@link Log} integer value of level represents priority of message. + * + * @return Integer level. + */ + public int getPriority() { + return priority; + } + + /** + * Compares priorities of LcLevels and returns if current is less than another. + * + * @param logLevel {@link LcLevel} to compare priority with; + * @return True if current level priority less than level passed as parameter. + */ + public boolean lessThan(@NonNull final LcLevel logLevel) { + return this.priority < logLevel.priority; + } + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java b/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java new file mode 100644 index 0000000..6cb34a9 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/log/LogProcessor.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.log; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * Abstract object to intercept log messages coming from {@link LcGroup} and {@link Lc} log methods. + */ +public abstract class LogProcessor { + + @NonNull + private final LcLevel minLogLevel; + + public LogProcessor(@NonNull final LcLevel minLogLevel) { + this.minLogLevel = minLogLevel; + } + + /** + * Minimum logging level. + * Any messages with lower priority won't be passed into {@link #processLogMessage(LcGroup, LcLevel, String, String, Throwable)}. + * + * @return Minimum log level represented by {@link LcLevel} object. + */ + @NonNull + public LcLevel getMinLogLevel() { + return minLogLevel; + } + + /** + * Core method to process any incoming log messages from {@link LcGroup} and {@link Lc} with level higher or equals {@link #getMinLogLevel()}. + * + * @param group {@link LcGroup} where log message came from; + * @param level {@link LcLevel} level (priority) of message; + * @param tag String mark of message; + * @param message Message to log; + * @param throwable Exception to log. + */ + public abstract void processLogMessage(@NonNull final LcGroup group, @NonNull final LcLevel level, + @NonNull final String tag, @NonNull final String message, @Nullable final Throwable throwable); + +} diff --git a/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java b/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java new file mode 100644 index 0000000..b3f11f0 --- /dev/null +++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/ShouldNotHappenException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.NonNull; + +/** + * Created by Gavriil Sitnikov on 13/11/2015. + * Exception that should be threw when some unexpected code reached. + * E.g. if some value null but it is not legal or in default case in switch if all specific cases should be processed. + */ +public class ShouldNotHappenException extends RuntimeException { + + private static final long serialVersionUID = 0; + + public ShouldNotHappenException() { + super(); + } + + public ShouldNotHappenException(@NonNull final String detailMessage) { + super(detailMessage); + } + + public ShouldNotHappenException(@NonNull final String detailMessage, @NonNull final Throwable throwable) { + super(detailMessage, throwable); + } + + public ShouldNotHappenException(@NonNull final Throwable throwable) { + super(throwable); + } + +} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java b/logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java similarity index 51% rename from src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java rename to logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java index 2dfb580..62cf778 100644 --- a/src/main/java/ru/touchin/roboswag/components/navigation/OnFragmentStartedListener.java +++ b/logging/src/main/java/ru/touchin/roboswag/core/utils/ThreadLocalValue.java @@ -17,23 +17,45 @@ * */ -package ru.touchin.roboswag.components.navigation; +package ru.touchin.roboswag.core.utils; import android.support.annotation.NonNull; -import android.support.v4.app.Fragment; /** - * Created by Gavriil Sitnikov on 08/10/2014. - * Base interface to listen child fragments start. - * Usually it helps to determine that fragment have showed on screen and we can change {@link android.app.Activity}'s navigation state for example. + * Created by Gavriil Sitnikov on 13/11/2015. + * Thread local value with specified creator of value per thread. */ -public interface OnFragmentStartedListener { +public class ThreadLocalValue extends ThreadLocal { + + @NonNull + private final Fabric fabric; + + public ThreadLocalValue(@NonNull final Fabric fabric) { + super(); + this.fabric = fabric; + } + + @NonNull + @Override + protected T initialValue() { + return fabric.create(); + } /** - * Calls by child fragment (added via {@link android.support.v4.app.FragmentManager}) on it'sstart. + * Fabric of thread-local objects. * - * @param fragment Child fragment which called this method. + * @param Type of objects. */ - void onFragmentStarted(@NonNull Fragment fragment); + public interface Fabric { -} \ No newline at end of file + /** + * Creates object. + * + * @return new instance of object. + */ + @NonNull + T create(); + + } + +} diff --git a/modules.gradle b/modules.gradle new file mode 100644 index 0000000..750126e --- /dev/null +++ b/modules.gradle @@ -0,0 +1,30 @@ +final String rootDir +if (gradle.ext.has('componentsRoot')) { + rootDir = gradle.ext['componentsRoot'] +} else { + rootDir = settingsDir +} + +include ':logging' +include ':utils' +include ':navigation' +include ':storable' +include ':api-logansquare' +include ':lifecycle-common' +include ':lifecycle-rx' +include ':views' +include ':recyclerview-adapters' +include ':kotlin-extensions' +include ':templates' + +project(':utils').projectDir = new File(rootDir, 'utils') +project(':logging').projectDir = new File(rootDir, 'logging') +project(':navigation').projectDir = new File(rootDir, 'navigation') +project(':storable').projectDir = new File(rootDir, 'storable') +project(':api-logansquare').projectDir = new File(rootDir, 'api-logansquare') +project(':lifecycle-common').projectDir = new File(rootDir, 'lifecycle-common') +project(':lifecycle-rx').projectDir = new File(rootDir, 'lifecycle-rx') +project(':views').projectDir = new File(rootDir, 'views') +project(':recyclerview-adapters').projectDir = new File(rootDir, 'recyclerview-adapters') +project(':kotlin-extensions').projectDir = new File(rootDir, 'kotlin-extensions') +project(':templates').projectDir = new File(rootDir, 'templates') diff --git a/navigation/.gitignore b/navigation/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/navigation/.gitignore @@ -0,0 +1 @@ +/build diff --git a/navigation/build.gradle b/navigation/build.gradle new file mode 100644 index 0000000..8a95e1c --- /dev/null +++ b/navigation/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":utils") + api project(":logging") + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "com.android.support:appcompat-v7:$versions.supportLibrary" +} diff --git a/navigation/src/main/AndroidManifest.xml b/navigation/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bd2d3ee --- /dev/null +++ b/navigation/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt new file mode 100644 index 0000000..7bf87c5 --- /dev/null +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.kt @@ -0,0 +1,263 @@ +/* + * 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.navigation + +import android.content.Context +import android.os.Bundle +import android.support.annotation.IdRes +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentTransaction +import android.view.MenuItem + +import ru.touchin.roboswag.core.log.Lc + +/** + * Created by Gavriil Sitnikov on 07/03/2016. + * Navigation which is controlling fragments on activity using [android.support.v4.app.FragmentManager]. + * Basically there are 4 main actions to add fragments to activity. + * 1) [.setInitial] means to set fragment on top and remove all previously added fragments from stack; + * 2) [.push] means to simply add fragment on top of the stack; + * 3) [.setAsTop] means to push fragment on top of the stack with specific [.TOP_FRAGMENT_TAG_MARK] tag. + * It is useful to realize up/back navigation: if [.up] method will be called then stack will go to nearest fragment with TOP tag. + * If [.back] method will be called then stack will go to previous fragment. + * Usually such logic using to set as top fragments from sidebar and show hamburger when some of them appeared; + * 4) [.pushForResult] means to push fragment with target fragment. It is also adding [.WITH_TARGET_FRAGMENT_TAG_MARK] tag. + * Also if such up/back navigation logic is not OK then [.backTo] method could be used with any condition to back to. + * In that case in any stack-change method it is allowed to setup fragment transactions. + */ +open class FragmentNavigation( + private val context: Context, + private val fragmentManager: FragmentManager, + @IdRes private val containerViewId: Int, + private val transition: Int = FragmentTransaction.TRANSIT_FRAGMENT_OPEN +) { + + companion object { + const val TOP_FRAGMENT_TAG_MARK = "TOP_FRAGMENT" + } + + /** + * Returns if last fragment in stack is top (added by [.setAsTop] or [.setInitial]) like fragment from sidebar menu. + * + * @return True if last fragment on stack has TOP_FRAGMENT_TAG_MARK. + */ + fun isCurrentFragmentTop(): Boolean { + if (fragmentManager.backStackEntryCount == 0) { + return true + } + val topFragmentTag = fragmentManager + .getBackStackEntryAt(fragmentManager.backStackEntryCount - 1) + .name + return topFragmentTag != null && topFragmentTag.contains(TOP_FRAGMENT_TAG_MARK) + } + + /** + * Allowed to react on [android.app.Activity]'s menu item selection. + * + * @param item Selected menu item; + * @return True if reaction fired. + */ + fun onOptionsItemSelected(item: MenuItem): Boolean = item.itemId == android.R.id.home && back() + + /** + * Base method which is adding fragment to stack. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment]; + * @param addToStack Flag to add this transaction to the back stack; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param backStackTag Tag of [Fragment] in back stack; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun addToStack( + fragmentClass: Class, + targetFragment: Fragment?, + targetRequestCode: Int, + addToStack: Boolean, + args: Bundle?, + backStackTag: String?, + transactionSetup: ((FragmentTransaction) -> Unit)? + ) { + if (fragmentManager.isDestroyed) { + Lc.assertion("FragmentManager is destroyed") + return + } + + val fragment = Fragment.instantiate(context, fragmentClass.name, args) + if (targetFragment != null) { + if (fragmentManager !== targetFragment.fragmentManager) { + Lc.assertion("FragmentManager of target is differ then of creating fragment. Target will be lost after restoring activity. " + + targetFragment.fragmentManager + " != " + fragmentManager) + } + fragment.setTargetFragment(targetFragment, targetRequestCode) + } + + val fragmentTransaction = fragmentManager.beginTransaction() + transactionSetup?.invoke(fragmentTransaction) + fragmentTransaction.replace(containerViewId, fragment, null) + if (addToStack) { + fragmentTransaction.addToBackStack(backStackTag).setTransition(transition) + } else { + fragmentTransaction.setPrimaryNavigationFragment(fragment) + } + fragmentTransaction.commit() + } + + /** + * Simply calls [FragmentManager.popBackStack]. + * + * @return True if it have back to some entry in stack. + */ + fun back(): Boolean { + if (fragmentManager.backStackEntryCount >= 1) { + fragmentManager.popBackStack() + return true + } + return false + } + + /** + * Backs to fragment which back stack's entry satisfy to specific condition. + * + * @param condition Condition of back stack entry to be satisfied; + * @return True if it have back to some entry in stack. + */ + fun backTo(condition: (FragmentManager.BackStackEntry) -> Boolean): Boolean { + val stackSize = fragmentManager.backStackEntryCount + var id: Int? = null + for (i in stackSize - 2 downTo 0) { + val backStackEntry = fragmentManager.getBackStackEntryAt(i) + if (condition(backStackEntry)) { + id = backStackEntry.id + break + } + } + if (id != null) { + fragmentManager.popBackStack(id, 0) + return true + } + return false + } + + /** + * Backs to fragment with specific [.TOP_FRAGMENT_TAG_MARK] tag. + * This tag is adding if fragment added to stack via [.setInitial] or [.setAsTop] methods. + * It can be used to create simple up/back navigation. + * + * @return True if it have back to some entry in stack. + */ + fun up() { + if (!backTo { backStackEntry -> backStackEntry.name != null && backStackEntry.name.endsWith(TOP_FRAGMENT_TAG_MARK) }) { + fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + } + + /** + * Pushes [Fragment] on top of stack with specific arguments and transaction setup. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun push( + fragmentClass: Class, + args: Bundle? = null, + addToStack: Boolean = true, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack(fragmentClass, null, 0, addToStack, args, null, transactionSetup) + } + + /** + * Pushes [Fragment] on top of stack with specific target fragment, arguments and transaction setup. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param targetFragment Target fragment to be set as [Fragment.getTargetFragment] of instantiated [Fragment]; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun pushForResult( + fragmentClass: Class, + targetFragment: Fragment, + targetRequestCode: Int, + args: Bundle? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack( + fragmentClass, + targetFragment, + targetRequestCode, + true, + args, + null, + transactionSetup + ) + } + + /** + * Pushes [Fragment] on top of stack with specific transaction setup, arguments + * and with [.TOP_FRAGMENT_TAG_MARK] tag used for simple up/back navigation. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + fun setAsTop( + fragmentClass: Class, + args: Bundle? = null, + addToStack: Boolean = true, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack(fragmentClass, null, 0, addToStack, args, "${fragmentClass.name};$TOP_FRAGMENT_TAG_MARK", transactionSetup) + } + + /** + * Pops all [Fragment]s and places new initial [Fragment] on top of stack with specific transaction setup and arguments. + * + * @param fragmentClass Class of [Fragment] to instantiate; + * @param args Bundle to be set as [Fragment.getArguments] of instantiated [Fragment]; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. + */ + @JvmOverloads + fun setInitial( + fragmentClass: Class, + args: Bundle? = null, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + beforeSetInitialActions() + setAsTop(fragmentClass, args, false, transactionSetup) + } + + /** + * Method calls every time before initial [Fragment] will be placed. + */ + protected fun beforeSetInitialActions() { + if (fragmentManager.isDestroyed) { + Lc.assertion("FragmentManager is destroyed") + return + } + + if (fragmentManager.backStackEntryCount > 0) { + fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + } + } + +} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/SerializableBundle.java diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java similarity index 95% rename from src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java rename to navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java index 423fc98..090f9c0 100644 --- a/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/SimpleActionBarDrawerToggle.java @@ -29,13 +29,15 @@ import android.view.MenuItem; import android.view.View; import ru.touchin.roboswag.components.navigation.activities.BaseActivity; +import ru.touchin.roboswag.components.navigation.activities.OnBackPressedListener; +import ru.touchin.roboswag.components.utils.UiUtils; /** * Created by Gavriil Sitnikov on 11/03/16. * Simple realization of one-side {@link ActionBarDrawerToggle}. */ public class SimpleActionBarDrawerToggle extends ActionBarDrawerToggle - implements FragmentManager.OnBackStackChangedListener, BaseActivity.OnBackPressedListener { + implements FragmentManager.OnBackStackChangedListener, OnBackPressedListener { @NonNull private final BaseActivity activity; @@ -206,7 +208,7 @@ public class SimpleActionBarDrawerToggle extends ActionBarDrawerToggle @Override public void onDrawerClosed(@NonNull final View view) { if (isInvalidateOptionsMenuSupported) { - activity.supportInvalidateOptionsMenu(); + activity.invalidateOptionsMenu(); } } @@ -221,9 +223,9 @@ public class SimpleActionBarDrawerToggle extends ActionBarDrawerToggle @Override public void onDrawerOpened(@NonNull final View drawerView) { - activity.hideSoftInput(); + UiUtils.OfViews.hideSoftInput(activity); if (isInvalidateOptionsMenuSupported) { - activity.supportInvalidateOptionsMenu(); + activity.invalidateOptionsMenu(); } } @@ -245,4 +247,4 @@ public class SimpleActionBarDrawerToggle extends ActionBarDrawerToggle super.onDrawerSlide(drawerView, this.slideOffset); } -} \ No newline at end of file +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java new file mode 100644 index 0000000..2e09da4 --- /dev/null +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java @@ -0,0 +1,121 @@ +/* + * 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.navigation.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.util.ArraySet; +import android.support.v7.app.AppCompatActivity; + +import java.util.Set; + +import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; + +/** + * Created by Gavriil Sitnikov on 08/03/2016. + * Base activity to use in components repository. + */ +public abstract class BaseActivity extends AppCompatActivity { + + @NonNull + private final Set onBackPressedListeners = new ArraySet<>(); + + @Override + protected void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + 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); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this) + " requestCode: " + requestCode + "; resultCode: " + resultCode); + } + + @Override + protected void onStart() { + super.onStart(); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + } + + @Override + protected void onResume() { + super.onResume(); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + } + + @Override + protected void onPause() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + super.onPause(); + } + + @Override + protected void onSaveInstanceState(@NonNull final Bundle stateToSave) { + super.onSaveInstanceState(stateToSave); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + } + + @Override + protected void onStop() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + super.onStop(); + } + + @Override + protected void onDestroy() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + super.onDestroy(); + } + + @Override + public boolean onSupportNavigateUp() { + onBackPressed(); + return true; + } + + public void addOnBackPressedListener(@NonNull final OnBackPressedListener onBackPressedListener) { + onBackPressedListeners.add(onBackPressedListener); + } + + public void removeOnBackPressedListener(@NonNull final OnBackPressedListener onBackPressedListener) { + onBackPressedListeners.remove(onBackPressedListener); + } + + @Override + public void onBackPressed() { + for (final OnBackPressedListener onBackPressedListener : onBackPressedListeners) { + if (onBackPressedListener.onBackPressed()) { + return; + } + } + super.onBackPressed(); + } + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java new file mode 100644 index 0000000..0f052f2 --- /dev/null +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/activities/OnBackPressedListener.java @@ -0,0 +1,15 @@ +package ru.touchin.roboswag.components.navigation.activities; + +/** + * Interface to be implemented for someone who want to intercept device back button pressing event. + */ +public interface OnBackPressedListener { + + /** + * Calls when user presses device back button. + * + * @return True if it is processed by this object. + */ + boolean onBackPressed(); + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java new file mode 100644 index 0000000..cb38f7a --- /dev/null +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java @@ -0,0 +1,430 @@ +/* + * 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.navigation.fragments; + +import android.animation.Animator; +import android.annotation.SuppressLint; +import android.arch.lifecycle.Lifecycle; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.CallSuper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.widget.FrameLayout; + +import java.lang.reflect.Constructor; + +import ru.touchin.roboswag.components.navigation.BuildConfig; +import ru.touchin.roboswag.components.navigation.viewcontrollers.ViewController; +import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Gavriil Sitnikov on 21/10/2015. + * Fragment instantiated in specific activity of {@link TActivity} type that is holding {@link ViewController} inside. + * + * @param Type of object which is representing it's fragment state; + * @param Type of {@link FragmentActivity} where fragment could be attached to. + */ +@SuppressWarnings("PMD.TooManyMethods") +public class ViewControllerFragment extends Fragment { + + private static final String VIEW_CONTROLLER_CLASS_EXTRA = "VIEW_CONTROLLER_CLASS_EXTRA"; + private static final String VIEW_CONTROLLER_STATE_EXTRA = "VIEW_CONTROLLER_STATE_EXTRA"; + + private static long acceptableUiCalculationTime = 100; + + /** + * Sets acceptable UI calculation time so there will be warnings in logs if ViewController's inflate/layout actions will take more than that time. + * It's 100ms by default. + */ + public static void setAcceptableUiCalculationTime(final long acceptableUiCalculationTime) { + ViewControllerFragment.acceptableUiCalculationTime = acceptableUiCalculationTime; + } + + @NonNull + private static T reserialize(@NonNull final T parcelable) { + Parcel parcel = Parcel.obtain(); + parcel.writeParcelable(parcelable, 0); + final byte[] serializableBytes = parcel.marshall(); + parcel.recycle(); + parcel = Parcel.obtain(); + parcel.unmarshall(serializableBytes, 0, serializableBytes.length); + parcel.setDataPosition(0); + final T result = parcel.readParcelable(Thread.currentThread().getContextClassLoader()); + parcel.recycle(); + return result; + } + + /** + * Creates {@link Bundle} which will store state. + * + * @param state State to use into ViewController. + * @return Returns bundle with state inside. + */ + @NonNull + public static Bundle args(@NonNull final Class viewControllerClass, @Nullable final Parcelable state) { + final Bundle result = new Bundle(); + result.putSerializable(VIEW_CONTROLLER_CLASS_EXTRA, viewControllerClass); + result.putParcelable(VIEW_CONTROLLER_STATE_EXTRA, state); + return result; + } + + @Nullable + private ViewController viewController; + private Class> viewControllerClass; + private TState state; + @Nullable + private ActivityResult pendingActivityResult; + + private boolean appeared; + + /** + * Returns specific {@link Parcelable} which contains state of fragment and it's {@link ViewController}. + * + * @return Object represents state. + */ + @NonNull + public TState getState() { + return state; + } + + @NonNull + public Class> getViewControllerClass() { + return viewControllerClass; + } + + @SuppressWarnings("unchecked") + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setHasOptionsMenu(!isChildFragment()); + + viewControllerClass = (Class>) getArguments().getSerializable(VIEW_CONTROLLER_CLASS_EXTRA); + state = savedInstanceState != null + ? savedInstanceState.getParcelable(VIEW_CONTROLLER_STATE_EXTRA) + : (getArguments() != null ? getArguments().getParcelable(VIEW_CONTROLLER_STATE_EXTRA) : null); + if (state != null) { + if (BuildConfig.DEBUG) { + state = reserialize(state); + } + } else { + Lc.assertion("State is required and null"); + } + } + + @NonNull + @Override + public final View onCreateView( + @NonNull final LayoutInflater inflater, + @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState + ) { + return new PlaceholderView(inflater.getContext(), viewControllerClass.getName()); + } + + @Override + public void onActivityCreated(@Nullable final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + //noinspection ConstantConditions + viewController = createViewController(requireActivity(), (ViewGroup) getView(), savedInstanceState); + viewController.onCreate(); + if (pendingActivityResult != null) { + viewController.onActivityResult(pendingActivityResult.requestCode, pendingActivityResult.resultCode, pendingActivityResult.data); + pendingActivityResult = null; + } + } + + @Nullable + @Override + public Animation onCreateAnimation(final int transit, final boolean enter, final int nextAnim) { + if (viewController != null) { + return viewController.onCreateAnimation(transit, enter, nextAnim); + } else { + return null; + } + } + + @Nullable + @Override + public Animator onCreateAnimator(final int transit, final boolean enter, final int nextAnim) { + if (viewController != null) { + return viewController.onCreateAnimator(transit, enter, nextAnim); + } else { + return null; + } + } + + @Override + public void onViewStateRestored(@Nullable final Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + if (viewController != null) { + viewController.onViewStateRestored(savedInstanceState); + } + } + + @SuppressLint("RestrictedApi") + @Override + public void onStart() { + super.onStart(); + if (!appeared && isMenuVisible()) { + onAppear(); + } + if (viewController != null) { + viewController.onStart(); + } + } + + /** + * Called when fragment is moved in started state and it's {@link #isMenuVisible()} sets to true. + * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. + */ + @CallSuper + protected void onAppear() { + appeared = true; + if (viewController != null) { + viewController.onAppear(); + } + } + + @Override + public void onResume() { + super.onResume(); + if (viewController != null) { + viewController.onResume(); + } + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + if (viewController != null) { + viewController.onLowMemory(); + } + } + + @Override + public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + if (viewController != null) { + viewController.onCreateOptionsMenu(menu, inflater); + } + } + + @Override + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + return (viewController != null && viewController.onOptionsItemSelected(item)) || super.onOptionsItemSelected(item); + } + + @Override + public void onPause() { + super.onPause(); + if (viewController != null) { + viewController.onPause(); + } + } + + @Override + public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { + super.onSaveInstanceState(savedInstanceState); + if (viewController != null) { + viewController.onSaveInstanceState(savedInstanceState); + } + savedInstanceState.putParcelable(VIEW_CONTROLLER_STATE_EXTRA, state); + } + + /** + * Called when fragment is moved in stopped state or it's {@link #isMenuVisible()} sets to false. + * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. + */ + @CallSuper + protected void onDisappear() { + appeared = false; + if (viewController != null) { + viewController.onDisappear(); + } + } + + @Override + public void onStop() { + if (appeared) { + onDisappear(); + } + if (viewController != null) { + viewController.onStop(); + } + super.onStop(); + } + + @Override + public void onDestroyView() { + if (viewController != null) { + viewController.onDestroy(); + viewController = null; + } + super.onDestroyView(); + } + + @Override + public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { + if (viewController != null) { + viewController.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + @Override + public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { + if (viewController != null) { + viewController.onActivityResult(requestCode, resultCode, data); + } else { + pendingActivityResult = new ActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void setMenuVisibility(final boolean menuVisible) { + super.setMenuVisibility(menuVisible); + if (getActivity() != null && getView() != null) { + final boolean started = getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED); + if (!appeared && menuVisible && started) { + onAppear(); + } + if (appeared && (!menuVisible || !started)) { + onDisappear(); + } + } + } + + /** + * Returns if fragment have parent fragment. + * + * @return Returns true if fragment is in some fragment's children stack. + */ + public boolean isChildFragment() { + return getParentFragment() != null; + } + + @NonNull + private ViewController createViewController( + @NonNull final FragmentActivity activity, + @NonNull final ViewGroup view, + @Nullable final Bundle savedInstanceState + ) { + if (viewControllerClass.getConstructors().length != 1) { + throw new ShouldNotHappenException("There should be single constructor for " + viewControllerClass); + } + final Constructor constructor = viewControllerClass.getConstructors()[0]; + final ViewController.CreationContext creationContext = new ViewController.CreationContext(activity, this, view); + final long creationTime = BuildConfig.DEBUG ? SystemClock.elapsedRealtime() : 0; + try { + switch (constructor.getParameterTypes().length) { + case 2: + return (ViewController) constructor.newInstance(creationContext, savedInstanceState); + case 3: + return (ViewController) constructor.newInstance(this, creationContext, savedInstanceState); + default: + throw new ShouldNotHappenException("Wrong constructor parameters count: " + constructor.getParameterTypes().length); + } + } catch (@NonNull final Exception exception) { + throw new ShouldNotHappenException(exception); + } finally { + checkCreationTime(creationTime); + } + } + + private void checkCreationTime(final long creationTime) { + if (BuildConfig.DEBUG) { + final long creationPeriod = SystemClock.elapsedRealtime() - creationTime; + if (creationPeriod > acceptableUiCalculationTime) { + LcGroup.UI_METRICS.w("Creation of %s took too much: %dms", viewControllerClass, creationPeriod); + } + } + } + + @NonNull + @Override + public String toString() { + return super.toString() + " ViewController: " + getViewControllerClass(); + } + + private static class PlaceholderView extends FrameLayout { + + @NonNull + private final String tagName; + private long lastMeasureTime; + + public PlaceholderView(@NonNull final Context context, @NonNull final String tagName) { + super(context); + this.tagName = tagName; + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (BuildConfig.DEBUG && lastMeasureTime == 0) { + lastMeasureTime = SystemClock.uptimeMillis(); + } + } + + @Override + protected void onDraw(@NonNull final Canvas canvas) { + super.onDraw(canvas); + if (BuildConfig.DEBUG && lastMeasureTime > 0) { + final long layoutTime = SystemClock.uptimeMillis() - lastMeasureTime; + if (layoutTime > acceptableUiCalculationTime) { + LcGroup.UI_METRICS.w("Measure and layout of %s took too much: %dms", tagName, layoutTime); + } + lastMeasureTime = 0; + } + } + + } + + private static class ActivityResult { + public final int requestCode; + public final int resultCode; + @Nullable + public final Intent data; + + ActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { + this.requestCode = requestCode; + this.resultCode = resultCode; + this.data = data; + } + } + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt new file mode 100644 index 0000000..0fae24f --- /dev/null +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/DefaultViewController.kt @@ -0,0 +1,21 @@ +package ru.touchin.roboswag.components.navigation.viewcontrollers + +import android.os.Bundle +import android.os.Parcelable +import android.support.annotation.LayoutRes +import android.support.v4.app.FragmentActivity + +abstract class DefaultViewController( + @LayoutRes layoutRes: Int, + creationContext: CreationContext, + savedInstanceState: Bundle? +) : ViewController( + creationContext, + savedInstanceState +) { + + init { + setContentView(layoutRes) + } + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt new file mode 100644 index 0000000..12424bc --- /dev/null +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/EmptyState.kt @@ -0,0 +1,19 @@ +package ru.touchin.roboswag.components.navigation.viewcontrollers + +import android.os.Parcel +import android.os.Parcelable + +object EmptyState : Parcelable { + + override fun writeToParcel(parcel: Parcel, flags: Int) = Unit + + override fun describeContents() = 0 + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel) = EmptyState + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java new file mode 100644 index 0000000..0aa771b --- /dev/null +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewController.java @@ -0,0 +1,471 @@ +/* + * 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.navigation.viewcontrollers; + +import android.animation.Animator; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.LifecycleRegistry; +import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.CallSuper; +import android.support.annotation.ColorInt; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.IdRes; +import android.support.annotation.LayoutRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.content.ContextCompat; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; + +import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment; +import ru.touchin.roboswag.components.utils.UiUtils; +import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; + +/** + * Created by Gavriil Sitnikov on 21/10/2015. + * Class to control view of specific fragment, activity and application by logic bridge. + * + * @param Type of activity where such {@link ViewController} could be; + * @param Type of state; + */ +public class ViewController implements LifecycleOwner { + + @NonNull + private final LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this); + @NonNull + private final TActivity activity; + @NonNull + private final ViewControllerFragment fragment; + @NonNull + private final ViewGroup container; + + @SuppressWarnings({"unchecked", "PMD.UnusedFormalParameter"}) + //UnusedFormalParameter: savedInstanceState could be used by children + public ViewController(@NonNull final CreationContext creationContext, @Nullable final Bundle savedInstanceState) { + this.activity = (TActivity) creationContext.activity; + this.fragment = creationContext.fragment; + this.container = creationContext.container; + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + return lifecycleRegistry; + } + + /** + * Returns activity where {@link ViewController} could be. + * + * @return Returns activity. + */ + @NonNull + public final TActivity getActivity() { + return activity; + } + + /** + * Returns fragment where {@link ViewController} could be. + * + * @return Returns fragment. + */ + @NonNull + public final ViewControllerFragment getFragment() { + return fragment; + } + + /** + * Returns state from fragment. + * + * @return Returns state. + */ + @NonNull + protected final TState getState() { + return fragment.getState(); + } + + /** + * Returns view instantiated in {@link #getFragment()} fragment attached to {@link #getActivity()} activity. + * Use it to inflate your views into at construction of this {@link ViewController}. + * + * @return Returns view. + */ + @NonNull + protected final ViewGroup getContainer() { + return container; + } + + /** + * Set the view controller content from a layout resource. + * This layout is placed directly into the container's ({@link #getContainer()}) view hierarchy. + * + * @param layoutResId Resource ID to be inflated. + */ + protected final void setContentView(@LayoutRes final int layoutResId) { + if (getContainer().getChildCount() > 0) { + getContainer().removeAllViews(); + } + UiUtils.inflateAndAdd(layoutResId, getContainer()); + } + + /** + * Set the view controller content to an explicit view. + * This view is placed directly into the container's ({@link #getContainer()}) view hierarchy. + * + * @param view The desired content to display. + */ + protected final void setContentView(@NonNull final View view) { + setContentView(view, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + } + + /** + * Set the view controller content to an explicit view with specific layout parameters. + * This view is placed directly into the container's ({@link #getContainer()}) view hierarchy. + * + * @param view The desired content to display; + * @param layoutParams Layout parameters for the view. + */ + protected final void setContentView(@NonNull final View view, @NonNull final ViewGroup.LayoutParams layoutParams) { + if (getContainer().getChildCount() > 0) { + getContainer().removeAllViews(); + } + getContainer().addView(view, layoutParams); + } + + /** + * Look for a child view with the given id. If this view has the given id, return this view. + * + * @param id The id to search for; + * @return The view that has the given id in the hierarchy. + */ + @NonNull + public final T findViewById(@IdRes final int id) { + return getContainer().findViewById(id); + } + + /** + * Return a localized, styled CharSequence from the application's package's + * default string table. + * + * @param resId Resource id for the CharSequence text + */ + @NonNull + public final CharSequence getText(@StringRes final int resId) { + return activity.getText(resId); + } + + /** + * Return a localized string from the application's package's default string table. + * + * @param resId Resource id for the string + */ + @NonNull + public final String getString(@StringRes final int resId) { + return activity.getString(resId); + } + + /** + * Return a localized formatted string from the application's package's default string table, substituting the format arguments as defined in + * {@link java.util.Formatter} and {@link java.lang.String#format}. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for substitution. + */ + @NonNull + public final String getString(@StringRes final int resId, @NonNull final Object... formatArgs) { + return activity.getString(resId, formatArgs); + } + + /** + * Return the color value associated with a particular resource ID. + * Starting in {@link android.os.Build.VERSION_CODES#M}, the returned + * color will be styled for the specified Context's theme. + * + * @param resId The resource id to search for data; + * @return int A single color value in the form 0xAARRGGBB. + */ + @ColorInt + public final int getColor(@ColorRes final int resId) { + return ContextCompat.getColor(activity, resId); + } + + /** + * Returns a color state list associated with a particular resource ID. + * + *

Starting in {@link android.os.Build.VERSION_CODES#M}, the returned + * color state list will be styled for the specified Context's theme. + * + * @param resId The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return A color state list, or {@code null} if the resource could not be resolved. + * @throws android.content.res.Resources.NotFoundException if the given ID + * does not exist. + */ + @Nullable + public final ColorStateList getColorStateList(@ColorRes final int resId) { + return ContextCompat.getColorStateList(activity, resId); + } + + /** + * Returns a drawable object associated with a particular resource ID. + * Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the + * returned drawable will be styled for the specified Context's theme. + * + * @param resId The resource id to search for data; + * @return Drawable An object that can be used to draw this resource. + */ + @Nullable + public final Drawable getDrawable(@DrawableRes final int resId) { + return ContextCompat.getDrawable(activity, resId); + } + + public final void startActivity(@NonNull final Intent intent) { + fragment.startActivity(intent); + } + + public final void startActivityForResult(@NonNull final Intent intent, final int requestCode) { + fragment.startActivityForResult(intent, requestCode); + } + + /** + * Calls when activity configuring ActionBar, Toolbar, Sidebar etc. + * If it will be called or not depends on {@link Fragment#hasOptionsMenu()} and {@link Fragment#isMenuVisible()}. + * + * @param menu The options menu in which you place your items; + * @param inflater Helper to inflate menu items. + */ + public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { + // do nothing + } + + /** + * Calls right after construction of {@link ViewController}. + * Happens at {@link ViewControllerFragment#onActivityCreated(Bundle)}. + */ + @CallSuper + public void onCreate() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + } + + /** + * Called when a fragment loads an animation. Note that if + * {@link FragmentTransaction#setCustomAnimations(int, int)} was called with + * {@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 nextAnim The resource set in + * {@link FragmentTransaction#setCustomAnimations(int, int)}, + * {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or + * 0 if neither was called. The value will depend on the current operation. + */ + @Nullable + public Animation onCreateAnimation(final int transit, final boolean enter, final int nextAnim) { + return null; + } + + /** + * Called when a fragment loads an animator. This will be called when + * {@link #onCreateAnimation(int, boolean, int)} returns null. Note that if + * {@link FragmentTransaction#setCustomAnimations(int, int)} was called with + * {@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 nextAnim The resource set in + * {@link FragmentTransaction#setCustomAnimations(int, int)}, + * {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or + * 0 if neither was called. The value will depend on the current operation. + */ + @Nullable + public Animator onCreateAnimator(final int transit, final boolean enter, final int nextAnim) { + return null; + } + + /** + * Calls when {@link ViewController} saved state has been restored into the view hierarchy. + * Happens at {@link ViewControllerFragment#onViewStateRestored}. + */ + @CallSuper + public void onViewStateRestored(@Nullable final Bundle savedInstanceState) { + // do nothing + } + + /** + * Calls when {@link ViewController} have started. + * Happens at {@link ViewControllerFragment#onStart()}. + */ + @CallSuper + public void onStart() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + UiUtils.OfViews.hideSoftInput(getContainer()); + } + + /** + * Called when fragment is moved in started state and it's {@link #getFragment().isMenuVisible()} sets to true. + * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. + */ + public void onAppear() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + } + + /** + * Calls when {@link ViewController} have resumed. + * Happens at {@link ViewControllerFragment#onResume()}. + */ + @CallSuper + public void onResume() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + } + + /** + * Calls when {@link ViewController} have goes near out of memory state. + * Happens at {@link ViewControllerFragment#onLowMemory()}. + */ + @CallSuper + public void onLowMemory() { + //do nothing + } + + /** + * Calls when {@link ViewController} have paused. + * Happens at {@link ViewControllerFragment#onPause()}. + */ + @CallSuper + public void onPause() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); + } + + /** + * Calls when {@link ViewController} should save it's state. + * Happens at {@link ViewControllerFragment#onSaveInstanceState(Bundle)}. + * Try not to use such method for saving state but use {@link ViewControllerFragment#getState()} from {@link #getFragment()}. + */ + @CallSuper + public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + } + + /** + * Called when fragment is moved in stopped state or it's {@link #getFragment().isMenuVisible()} sets to false. + * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. + */ + public void onDisappear() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + } + + /** + * Calls when {@link ViewController} have stopped. + * Happens at {@link ViewControllerFragment#onStop()}. + */ + @CallSuper + public void onStop() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + } + + /** + * Calls when {@link ViewController} have destroyed. + * Happens usually at {@link ViewControllerFragment#onDestroyView()}. In some cases at {@link ViewControllerFragment#onDestroy()}. + */ + @CallSuper + public void onDestroy() { + LcGroup.UI_LIFECYCLE.i(Lc.getCodePoint(this)); + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + } + + + /** + * Calls when {@link ViewController} have requested permissions results. + * Happens at {@link ViewControllerFragment#onRequestPermissionsResult(int, String[], int[])} ()}. + */ + @CallSuper + @SuppressWarnings("PMD.UseVarargs") + public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { + //do nothing + } + + /** + * Callback from parent fragment. + */ + public void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) { + // do nothing + } + + /** + * Similar to {@link ViewControllerFragment#onOptionsItemSelected(MenuItem)}. + * + * @param item Selected menu item; + * @return True if selection processed. + */ + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + return false; + } + + /* + * Helper class to simplify constructor override. + */ + public static class CreationContext { + + @NonNull + private final FragmentActivity activity; + @NonNull + private final ViewControllerFragment fragment; + @NonNull + private final ViewGroup container; + + public CreationContext( + @NonNull final FragmentActivity activity, + @NonNull final ViewControllerFragment fragment, + @NonNull final ViewGroup container + ) { + this.activity = activity; + this.fragment = fragment; + this.container = container; + } + + } + +} diff --git a/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt new file mode 100644 index 0000000..a8e4b87 --- /dev/null +++ b/navigation/src/main/java/ru/touchin/roboswag/components/navigation/viewcontrollers/ViewControllerNavigation.kt @@ -0,0 +1,146 @@ +/* + * 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.navigation.viewcontrollers + +import android.content.Context +import android.os.Parcelable +import android.support.annotation.IdRes +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentActivity +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentTransaction + +import ru.touchin.roboswag.components.navigation.FragmentNavigation +import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment + +/** + * Created by Gavriil Sitnikov on 07/03/2016. + * Navigation based on [ViewController]s which are creating by [Fragment]s. + * So basically it is just [FragmentNavigation] where most of fragments should be inherited from [ViewControllerFragment]. + * + * @param TActivity Type of activity where [ViewController]s should be showed. + */ +open class ViewControllerNavigation( + context: Context, + fragmentManager: FragmentManager, + @IdRes containerViewId: Int, + transition: Int = FragmentTransaction.TRANSIT_FRAGMENT_OPEN +) : FragmentNavigation(context, fragmentManager, containerViewId, transition) { + + /** + * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState] and with specific transaction setup. + * + * @param viewControllerClass Class of [ViewController] to be pushed; + * @param state [Parcelable] of [ViewController]'s fragment; + * @param addToStack Flag to add this transaction to the back stack; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; + * @param TState Type of state of fragment. + */ + fun pushViewController( + viewControllerClass: Class>, + state: TState, + addToStack: Boolean = true, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack( + ViewControllerFragment::class.java, + null, + 0, + addToStack, + ViewControllerFragment.args(viewControllerClass, state), + null, + transactionSetup + ) + } + + /** + * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState] + * and with specific [TTargetFragment] and transaction setup. + * + * @param viewControllerClass Class of [ViewController] to be pushed; + * @param targetFragment [ViewControllerFragment] to be set as target; + * @param state [Parcelable] of [ViewController]'s fragment; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; + * @param TState Type of state of fragment; + * @param TTargetFragment Type of target fragment. + */ + fun pushViewControllerForResult( + viewControllerClass: Class>, + state: TState, + targetFragment: TTargetFragment, + targetRequestCode: Int, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack( + ViewControllerFragment::class.java, + targetFragment, + targetRequestCode, + true, + ViewControllerFragment.args(viewControllerClass, state), + null, + transactionSetup + ) + } + + /** + * Pushes [ViewController] on top of stack with specific [ViewControllerFragment.getState] and with specific transaction setup + * and with [.TOP_FRAGMENT_TAG_MARK] tag used for simple up/back navigation. + * + * @param viewControllerClass Class of [ViewController] to be pushed; + * @param state [Parcelable] of [ViewController]'s fragment; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; + * @param TState Type of state of fragment. + */ + fun setViewControllerAsTop( + viewControllerClass: Class>, + state: TState, + addToStack: Boolean = true, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + addToStack( + ViewControllerFragment::class.java, + null, + 0, + addToStack, + ViewControllerFragment.args(viewControllerClass, state), + "${viewControllerClass.name};$TOP_FRAGMENT_TAG_MARK", + transactionSetup + ) + } + + /** + * Pops all [Fragment]s and places new initial [ViewController] on top of stack + * with specific [ViewControllerFragment.getState] and specific transaction setup. + * + * @param viewControllerClass Class of [ViewController] to be pushed; + * @param state [Parcelable] of [ViewController]'s fragment; + * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; + * @param TState Type of state of fragment. + */ + fun setInitialViewController( + viewControllerClass: Class>, + state: TState, + transactionSetup: ((FragmentTransaction) -> Unit)? = null + ) { + beforeSetInitialActions() + setViewControllerAsTop(viewControllerClass, state, false, transactionSetup) + } + +} diff --git a/recyclerview-adapters/.gitignore b/recyclerview-adapters/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/recyclerview-adapters/.gitignore @@ -0,0 +1 @@ +/build diff --git a/recyclerview-adapters/build.gradle b/recyclerview-adapters/build.gradle new file mode 100644 index 0000000..12fc082 --- /dev/null +++ b/recyclerview-adapters/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } +} + +dependencies { + api project(':kotlin-extensions') + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + + implementation "com.android.support:recyclerview-v7:$versions.supportLibrary" +} diff --git a/recyclerview-adapters/src/main/AndroidManifest.xml b/recyclerview-adapters/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2228245 --- /dev/null +++ b/recyclerview-adapters/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java new file mode 100644 index 0000000..25be0c9 --- /dev/null +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.components.adapters; + +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.RecyclerView; +import android.view.ViewGroup; + +import java.util.List; + +/** + * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. + * Default {@link #getItemViewType} is generating on construction of object. + * + * @param Type of {@link RecyclerView.ViewHolder} of delegate. + */ +public abstract class AdapterDelegate { + + private final int defaultItemViewType = ViewCompat.generateViewId(); + + /** + * Unique ID of AdapterDelegate. + * + * @return Unique ID. + */ + public int getItemViewType() { + return defaultItemViewType; + } + + /** + * Returns if object is processable by this delegate. + * + * @param items Items to check; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection; + * @return True if item is processable by this delegate. + */ + public abstract boolean isForViewType(@NonNull final List items, final int adapterPosition, final int collectionPosition); + + /** + * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. + * + * @param items Items in adapter; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection; + * @return Unique item ID. + */ + public long getItemId(@NonNull final List items, final int adapterPosition, final int collectionPosition) { + return 0; + } + + /** + * Creates ViewHolder to bind item to it later. + * + * @param parent Container of ViewHolder's view. + * @return New ViewHolder. + */ + @NonNull + public abstract TViewHolder onCreateViewHolder(@NonNull final ViewGroup parent); + + /** + * Binds item to created by this object ViewHolder. + * + * @param holder ViewHolder to bind item to; + * @param items Items in adapter; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection that contains item; + * @param payloads Payloads; + */ + public abstract void onBindViewHolder( + @NonNull final RecyclerView.ViewHolder holder, + @NonNull final List items, + final int adapterPosition, + final int collectionPosition, + @NonNull final List payloads + ); + +} diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt new file mode 100644 index 0000000..e3ba14f --- /dev/null +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/DelegatesManager.kt @@ -0,0 +1,52 @@ +package ru.touchin.roboswag.components.adapters + +import android.support.v7.widget.RecyclerView +import android.util.SparseArray +import android.view.ViewGroup + +/** + * Manager for delegation callbacks from [RecyclerView.Adapter] to delegates. + */ +class DelegatesManager { + + private val delegates = SparseArray>() + + fun getItemViewType(items: List<*>, adapterPosition: Int, collectionPosition: Int): Int { + for (index in 0 until delegates.size()) { + val delegate = delegates.valueAt(index) + if (delegate.isForViewType(items, adapterPosition, collectionPosition)) { + return delegate.itemViewType + } + } + throw IllegalStateException("Delegate not found for adapterPosition: $adapterPosition") + } + + fun getItemId(items: List<*>, adapterPosition: Int, collectionPosition: Int): Long { + val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition)) + return delegate.getItemId(items, adapterPosition, collectionPosition) + } + + fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = getDelegate(viewType).onCreateViewHolder(parent) + + fun onBindViewHolder(holder: RecyclerView.ViewHolder, items: List<*>, adapterPosition: Int, collectionPosition: Int, payloads: List) { + val delegate = getDelegate(getItemViewType(items, adapterPosition, collectionPosition)) + delegate.onBindViewHolder(holder, items, adapterPosition, collectionPosition, payloads) + } + + /** + * Adds [PositionAdapterDelegate] to adapter. + * + * @param delegate Delegate to add. + */ + fun addDelegate(delegate: AdapterDelegate<*>) = delegates.put(delegate.itemViewType, delegate) + + /** + * Removes [AdapterDelegate] from adapter. + * + * @param delegate Delegate to remove. + */ + fun removeDelegate(delegate: AdapterDelegate<*>) = delegates.remove(delegate.itemViewType) + + private fun getDelegate(viewType: Int) = delegates[viewType] ?: throw IllegalStateException("No AdapterDelegate added for view type: $viewType") + +} diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt new file mode 100644 index 0000000..094b298 --- /dev/null +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/DelegationListAdapter.kt @@ -0,0 +1,89 @@ +package ru.touchin.roboswag.components.adapters + +import android.support.v7.recyclerview.extensions.AsyncDifferConfig +import android.support.v7.recyclerview.extensions.AsyncListDiffer +import android.support.v7.util.DiffUtil +import android.support.v7.widget.RecyclerView +import android.view.ViewGroup +import ru.touchin.roboswag.components.extensions.setOnRippleClickListener + +/** + * Base adapter with delegation and diff computing on background thread. + */ +open class DelegationListAdapter(config: AsyncDifferConfig) : RecyclerView.Adapter() { + + constructor(diffCallback: DiffUtil.ItemCallback) : this(AsyncDifferConfig.Builder(diffCallback).build()) + + var itemClickListener: ((TItem, RecyclerView.ViewHolder) -> Unit)? = null + + private val delegatesManager = DelegatesManager() + private var differ = AsyncListDiffer(OffsetAdapterUpdateCallback(this, ::getHeadersCount), config) + + open fun getHeadersCount() = 0 + + open fun getFootersCount() = 0 + + override fun getItemCount() = getHeadersCount() + getList().size + getFootersCount() + + override fun getItemViewType(position: Int) = delegatesManager.getItemViewType(getList(), position, getCollectionPosition(position)) + + override fun getItemId(position: Int) = delegatesManager.getItemId(getList(), position, getCollectionPosition(position)) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = delegatesManager.onCreateViewHolder(parent, viewType) + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { + val collectionPosition = getCollectionPosition(position) + if (collectionPosition in 0 until getList().size) { + if (itemClickListener != null) { + holder.itemView.setOnRippleClickListener { + itemClickListener?.invoke(getList()[getCollectionPosition(holder.adapterPosition)], holder) + } + } else { + holder.itemView.setOnClickListener(null) + } + } + delegatesManager.onBindViewHolder(holder, getList(), position, collectionPosition, payloads) + } + + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = Unit + + /** + * Adds [AdapterDelegate] to adapter. + * + * @param delegate Delegate to add. + */ + fun addDelegate(delegate: AdapterDelegate<*>) = delegatesManager.addDelegate(delegate) + + /** + * Removes [AdapterDelegate] from adapter. + * + * @param delegate Delegate to remove. + */ + fun removeDelegate(delegate: AdapterDelegate<*>) = delegatesManager.removeDelegate(delegate) + + /** + * Submits a new list to be diffed, and displayed. + * + * If a list is already being displayed, a diff will be computed on a background thread, which + * will dispatch Adapter.notifyItem events on the main thread. + * + * @param list The new list to be displayed. + */ + fun submitList(list: List) = differ.submitList(list) + + /** + * Get the current List - any diffing to present this list has already been computed and + * dispatched via the ListUpdateCallback. + *

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

+ * The returned list may not be mutated - mutations to content must be done through + * {@link #submitList(List)}. + * + * @return current List. + */ + fun getList(): List = differ.currentList + + fun getCollectionPosition(adapterPosition: Int) = adapterPosition - getHeadersCount() + +} diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java new file mode 100644 index 0000000..324bcf4 --- /dev/null +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java @@ -0,0 +1,85 @@ +package ru.touchin.roboswag.components.adapters; + +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; + +import java.util.List; + +/** + * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. + * Such delegates are creating and binding ViewHolders for specific items. + * Default {@link #getItemViewType} is generating on construction of object. + * + * @param Type of {@link RecyclerView.ViewHolder} of delegate; + * @param Type of items to bind to {@link RecyclerView.ViewHolder}s. + */ +public abstract class ItemAdapterDelegate extends AdapterDelegate { + + @Override + public boolean isForViewType(@NonNull final List items, final int adapterPosition, final int collectionPosition) { + return collectionPosition >= 0 + && collectionPosition < items.size() + && isForViewType(items.get(collectionPosition), adapterPosition, collectionPosition); + } + + /** + * Returns if object is processable by this delegate. + * This item will be casted to {@link TItem} and passes to {@link #onBindViewHolder(TViewHolder, TItem, int, int, List)}. + * + * @param item Item to check; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection that contains item; + * @return True if item is processable by this delegate. + */ + public boolean isForViewType(@NonNull final Object item, final int adapterPosition, final int collectionPosition) { + return true; + } + + @Override + public long getItemId(@NonNull final List items, final int adapterPosition, final int collectionPosition) { + //noinspection unchecked + return getItemId((TItem) items.get(collectionPosition), adapterPosition, collectionPosition); + } + + /** + * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. + * + * @param item Item in adapter; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection that contains item; + * @return Unique item ID. + */ + public long getItemId(@NonNull final TItem item, final int adapterPosition, final int collectionPosition) { + return 0; + } + + @Override + public void onBindViewHolder( + @NonNull final RecyclerView.ViewHolder holder, + @NonNull final List items, + final int adapterPosition, + final int collectionPosition, + @NonNull final List payloads + ) { + //noinspection unchecked + onBindViewHolder((TViewHolder) holder, (TItem) items.get(collectionPosition), adapterPosition, collectionPosition, payloads); + } + + /** + * Binds item with payloads to created by this object ViewHolder. + * + * @param holder ViewHolder to bind item to; + * @param item Item in adapter; + * @param adapterPosition Position of item in adapter; + * @param collectionPosition Position of item in collection that contains item; + * @param payloads Payloads; + */ + public abstract void onBindViewHolder( + @NonNull final TViewHolder holder, + @NonNull final TItem item, + final int adapterPosition, + final int collectionPosition, + @NonNull final List payloads + ); + +} diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt new file mode 100644 index 0000000..9715eb2 --- /dev/null +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/OffsetAdapterUpdateCallback.kt @@ -0,0 +1,24 @@ +package ru.touchin.roboswag.components.adapters + +import android.support.v7.util.ListUpdateCallback +import android.support.v7.widget.RecyclerView + +class OffsetAdapterUpdateCallback(private val adapter: RecyclerView.Adapter<*>, private val offsetProvider: () -> Int) : ListUpdateCallback { + + override fun onInserted(position: Int, count: Int) { + adapter.notifyItemRangeInserted(position + offsetProvider(), count) + } + + override fun onRemoved(position: Int, count: Int) { + adapter.notifyItemRangeRemoved(position + offsetProvider(), count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + adapter.notifyItemMoved(fromPosition + offsetProvider(), toPosition + offsetProvider()) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + adapter.notifyItemRangeChanged(position + offsetProvider(), count, payload) + } + +} diff --git a/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java new file mode 100644 index 0000000..ada8888 --- /dev/null +++ b/recyclerview-adapters/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java @@ -0,0 +1,68 @@ +package ru.touchin.roboswag.components.adapters; + +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; + +import java.util.List; + +/** + * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. + * Such delegates are creating and binding ViewHolders by position in adapter. + * Default {@link #getItemViewType} is generating on construction of object. + * + * @param Type of {@link RecyclerView.ViewHolder} of delegate. + */ +public abstract class PositionAdapterDelegate extends AdapterDelegate { + + @Override + public boolean isForViewType(@NonNull final List items, final int adapterPosition, final int collectionPosition) { + return isForViewType(adapterPosition); + } + + /** + * Returns if object is processable by this delegate. + * + * @param adapterPosition Position of item in adapter; + * @return True if item is processable by this delegate. + */ + public abstract boolean isForViewType(final int adapterPosition); + + @Override + public long getItemId(@NonNull final List objects, final int adapterPosition, final int itemsOffset) { + return getItemId(adapterPosition); + } + + /** + * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. + * + * @param adapterPosition Position of item in adapter; + * @return Unique item ID. + */ + public long getItemId(final int adapterPosition) { + return 0; + } + + @Override + public void onBindViewHolder( + @NonNull final RecyclerView.ViewHolder holder, + @NonNull final List items, + final int adapterPosition, + final int collectionPosition, + @NonNull final List payloads + ) { + //noinspection unchecked + onBindViewHolder((TViewHolder) holder, adapterPosition, payloads); + } + + /** + * Binds position with payloads to ViewHolder. + * + * @param holder ViewHolder to bind position to; + * @param adapterPosition Position of item in adapter; + * @param payloads Payloads. + */ + public void onBindViewHolder(@NonNull final TViewHolder holder, final int adapterPosition, @NonNull final List payloads) { + //do nothing by default + } + +} diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..7a28aea --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + applicationId "ru.touchin.roboswag.components" + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + 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:$versions.supportLibrary" + implementation 'com.android.support.constraint:constraint-layout:1.1.2' +} + +ext.buildScriptsDir = "$rootDir/BuildScripts" +apply from: "$buildScriptsDir/gradle/staticAnalysis.gradle" diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/sample/proguard-rules.pro @@ -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 diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a99bb50 --- /dev/null +++ b/sample/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/sample/src/main/java/ru/touchin/roboswag/components/MainActivity.kt b/sample/src/main/java/ru/touchin/roboswag/components/MainActivity.kt new file mode 100644 index 0000000..ebf42b3 --- /dev/null +++ b/sample/src/main/java/ru/touchin/roboswag/components/MainActivity.kt @@ -0,0 +1,12 @@ +package ru.touchin.roboswag.components + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..3e810c0 --- /dev/null +++ b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..5713f34 --- /dev/null +++ b/sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..7539a01 --- /dev/null +++ b/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..f4bc9d9 --- /dev/null +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..f4bc9d9 --- /dev/null +++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a2f5908 Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1b52399 Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..ff10afd Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..115a4c7 Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..dcd3cd8 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..459ca60 Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..8ca12fe Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e19b41 Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..b824ebd Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4c19a13 Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml new file mode 100644 index 0000000..3ab3e9c --- /dev/null +++ b/sample/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml new file mode 100644 index 0000000..f6470ae --- /dev/null +++ b/sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Components + diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml new file mode 100644 index 0000000..5885930 --- /dev/null +++ b/sample/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c8a70f0 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +apply from: 'modules.gradle' + +include 'sample' diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml deleted file mode 100644 index c76023c..0000000 --- a/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java deleted file mode 100644 index 0bdf4d0..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/AdapterDelegate.java +++ /dev/null @@ -1,221 +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.view.ViewGroup; - -import ru.touchin.roboswag.components.utils.LifecycleBindable; -import ru.touchin.roboswag.components.utils.UiUtils; -import rx.Completable; -import rx.Observable; -import rx.Single; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; - -/** - * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. - * Default {@link #getItemViewType} is generating on construction of object. - * - * @param Type of {@link BindableViewHolder} of delegate. - */ -@SuppressWarnings("PMD.TooManyMethods") -//TooManyMethods: it's ok -public abstract class AdapterDelegate implements LifecycleBindable { - - @NonNull - private final LifecycleBindable parentLifecycleBindable; - private final int defaultItemViewType; - - public AdapterDelegate(@NonNull final LifecycleBindable parentLifecycleBindable) { - this.parentLifecycleBindable = parentLifecycleBindable; - this.defaultItemViewType = UiUtils.OfViews.generateViewId(); - } - - /** - * Returns parent {@link LifecycleBindable} that this delegate created from (e.g. Activity or ViewController). - * - * @return Parent {@link LifecycleBindable}. - */ - @NonNull - public LifecycleBindable getParentLifecycleBindable() { - return parentLifecycleBindable; - } - - /** - * Unique ID of AdapterDelegate. - * - * @return Unique ID. - */ - public int getItemViewType() { - return defaultItemViewType; - } - - /** - * 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); - - @SuppressWarnings("CPD-START") - //CPD: it is same as in other implementation based on BaseLifecycleBindable - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable) { - return parentLifecycleBindable.untilStop(observable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return parentLifecycleBindable.untilStop(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return parentLifecycleBindable.untilStop(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return parentLifecycleBindable.untilStop(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single) { - return parentLifecycleBindable.untilStop(single); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return parentLifecycleBindable.untilStop(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return parentLifecycleBindable.untilStop(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable) { - return parentLifecycleBindable.untilStop(completable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return parentLifecycleBindable.untilStop(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return parentLifecycleBindable.untilStop(completable, onCompletedAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable) { - return parentLifecycleBindable.untilDestroy(observable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return parentLifecycleBindable.untilDestroy(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return parentLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return parentLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single) { - return parentLifecycleBindable.untilDestroy(single); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return parentLifecycleBindable.untilDestroy(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return parentLifecycleBindable.untilDestroy(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable) { - return parentLifecycleBindable.untilDestroy(completable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return parentLifecycleBindable.untilDestroy(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return parentLifecycleBindable.untilDestroy(completable, onCompletedAction, onErrorAction); - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/BindableViewHolder.java b/src/main/java/ru/touchin/roboswag/components/adapters/BindableViewHolder.java deleted file mode 100644 index 19d48f7..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/BindableViewHolder.java +++ /dev/null @@ -1,263 +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.adapters; - -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.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import ru.touchin.roboswag.components.utils.LifecycleBindable; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; -import rx.Completable; -import rx.Observable; -import rx.Single; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; - -/** - * Created by Gavriil Sitnikov on 12/8/2016. - * ViewHolder that implements {@link LifecycleBindable} and uses parent bindable object as bridge (Activity, ViewController etc.). - */ -@SuppressWarnings("PMD.TooManyMethods") -public class BindableViewHolder extends RecyclerView.ViewHolder implements LifecycleBindable { - - @NonNull - private final LifecycleBindable baseLifecycleBindable; - - public BindableViewHolder(@NonNull final LifecycleBindable baseLifecycleBindable, @NonNull final View itemView) { - super(itemView); - this.baseLifecycleBindable = baseLifecycleBindable; - } - - /** - * Look for a child view with the given id. If this view has the given id, return this view. - * - * @param id The id to search for; - * @return The view that has the given id in the hierarchy. - */ - @NonNull - @SuppressWarnings("unchecked") - public T findViewById(@IdRes final int id) { - final T viewById = (T) itemView.findViewById(id); - if (viewById == null) { - throw new ShouldNotHappenException("No view for id=" + itemView.getResources().getResourceName(id)); - } - return viewById; - } - - /** - * Return the string value associated with a particular resource ID. It - * will be stripped of any styled text information. - * - * @param resId The resource id to search for data; - * @return String The string data associated with the resource. - */ - @NonNull - public String getString(@StringRes final int resId) { - return itemView.getResources().getString(resId); - } - - /** - * Return the string value associated with a particular resource ID. It - * will be stripped of any styled text information. - * - * @param resId The resource id to search for data; - * @param formatArgs The format arguments that will be used for substitution. - * @return String The string data associated with the resource. - */ - @NonNull - public String getString(@StringRes final int resId, @Nullable final Object... formatArgs) { - return itemView.getResources().getString(resId, formatArgs); - } - - /** - * Return the color value associated with a particular resource ID. - * Starting in {@link android.os.Build.VERSION_CODES#M}, the returned - * color will be styled for the specified Context's theme. - * - * @param resId The resource id to search for data; - * @return int A single color value in the form 0xAARRGGBB. - */ - @ColorInt - public int getColor(@ColorRes final int resId) { - return ContextCompat.getColor(itemView.getContext(), resId); - } - - /** - * Returns a drawable object associated with a particular resource ID. - * Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the - * returned drawable will be styled for the specified Context's theme. - * - * @param resId The resource id to search for data; - * @return Drawable An object that can be used to draw this resource. - */ - @NonNull - public Drawable getDrawable(@DrawableRes final int resId) { - return ContextCompat.getDrawable(itemView.getContext(), resId); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable) { - return baseLifecycleBindable.untilStop(observable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single) { - return baseLifecycleBindable.untilStop(single); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return baseLifecycleBindable.untilStop(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable) { - return baseLifecycleBindable.untilStop(completable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilStop(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(completable, onCompletedAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable) { - return baseLifecycleBindable.untilDestroy(observable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single) { - return baseLifecycleBindable.untilDestroy(single); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return baseLifecycleBindable.untilDestroy(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable) { - return baseLifecycleBindable.untilDestroy(completable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilDestroy(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(completable, onCompletedAction, onErrorAction); - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java deleted file mode 100644 index 033e944..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/ItemAdapterDelegate.java +++ /dev/null @@ -1,81 +0,0 @@ -package ru.touchin.roboswag.components.adapters; - -import android.support.annotation.NonNull; -import android.view.ViewGroup; - -import java.util.List; - -import ru.touchin.roboswag.components.utils.LifecycleBindable; - -/** - * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. - * Such delegates are creating and binding ViewHolders for specific items. - * Default {@link #getItemViewType} is generating on construction of object. - * - * @param Type of {@link BindableViewHolder} of delegate; - * @param Type of items to bind to {@link BindableViewHolder}s. - */ -public abstract class ItemAdapterDelegate extends AdapterDelegate { - - public ItemAdapterDelegate(@NonNull final LifecycleBindable parentLifecycleBindable) { - super(parentLifecycleBindable); - } - - /** - * 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)}. - * - * @param item Item to check; - * @param positionInAdapter Position of item in adapter; - * @param itemCollectionPosition Position of item in collection that contains item; - * @return True if item is processable by this delegate. - */ - public abstract boolean isForViewType(@NonNull final Object item, final int positionInAdapter, final int itemCollectionPosition); - - /** - * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. - * - * @param item Item to check; - * @param positionInAdapter Position of item in adapter; - * @param positionInCollection Position of item in collection that contains item; - * @return Unique item ID. - */ - public long getItemId(@NonNull final TItem item, final int positionInAdapter, final int positionInCollection) { - 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 item Item to check; - * @param positionInAdapter Position of item in adapter; - * @param positionInCollection Position of item in collection that contains item; - */ - public abstract void onBindViewHolder(@NonNull final TViewHolder holder, @NonNull final TItem item, - final int positionInAdapter, final int positionInCollection); - - /** - * Binds item with payloads to created by this object ViewHolder. - * - * @param holder ViewHolder to bind item to; - * @param item Item to check; - * @param payloads Payloads; - * @param positionInAdapter Position of item in adapter; - * @param positionInCollection Position of item in collection that contains item; - */ - public void onBindViewHolder(@NonNull final TViewHolder holder, @NonNull final TItem item, @NonNull final List payloads, - final int positionInAdapter, final int positionInCollection) { - //do nothing by default - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java b/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java deleted file mode 100644 index bdee224..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/ObservableCollectionAdapter.java +++ /dev/null @@ -1,681 +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.adapters; - -import android.os.Looper; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v7.widget.RecyclerView; -import android.view.ViewGroup; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import ru.touchin.roboswag.components.utils.LifecycleBindable; -import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.observables.collections.ObservableCollection; -import ru.touchin.roboswag.core.observables.collections.ObservableList; -import ru.touchin.roboswag.core.observables.collections.changes.Change; -import ru.touchin.roboswag.core.observables.collections.changes.ChangePayloadProducer; -import ru.touchin.roboswag.core.observables.collections.changes.CollectionChanges; -import ru.touchin.roboswag.core.observables.collections.changes.SameItemsPredicate; -import ru.touchin.roboswag.core.observables.collections.loadable.LoadingMoreList; -import ru.touchin.roboswag.core.utils.Optional; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; -import rx.Observable; -import rx.functions.Action1; -import rx.functions.Action2; -import rx.functions.Action3; -import rx.subjects.BehaviorSubject; - -/** - * Created by Gavriil Sitnikov on 20/11/2015. - * Adapter based on {@link ObservableCollection} and providing some useful features like: - * - item-based binding method; - * - delegates by {@link AdapterDelegate} over itemViewType logic; - * - item click listener setup by {@link #setOnItemClickListener(OnItemClickListener)}; - * - allows to inform about footers/headers by overriding base create/bind methods and {@link #getHeadersCount()} plus {@link #getFootersCount()}; - * - by default it is pre-loading items for collections like {@link ru.touchin.roboswag.core.observables.collections.loadable.LoadingMoreList}. - * - * @param Type of items to bind to ViewHolders; - * @param Type of ViewHolders to show items. - */ -@SuppressWarnings({"unchecked", "PMD.TooManyMethods"}) -//TooManyMethods: it's ok -public abstract class ObservableCollectionAdapter - extends RecyclerView.Adapter { - - private static final int PRE_LOADING_COUNT = 20; - - private static boolean inDebugMode; - - /** - * Enables debugging features like checking concurrent delegates. - */ - public static void setInDebugMode() { - inDebugMode = true; - } - - @NonNull - private final BehaviorSubject>> observableCollectionSubject - = BehaviorSubject.create(new Optional<>(null)); - @NonNull - private final BehaviorSubject moreAutoLoadingRequested = BehaviorSubject.create(); - @NonNull - private final LifecycleBindable lifecycleBindable; - @Nullable - private Object onItemClickListener; - private int lastUpdatedChangeNumber = -1; - - @NonNull - private final ObservableList innerCollection = new ObservableList<>(); - private boolean anyChangeApplied; - private long itemClickDelayMillis; - @NonNull - private final List attachedRecyclerViews = new LinkedList<>(); - @NonNull - private final List> delegates = new ArrayList<>(); - - public ObservableCollectionAdapter(@NonNull final LifecycleBindable lifecycleBindable) { - super(); - this.lifecycleBindable = lifecycleBindable; - lifecycleBindable.untilDestroy(innerCollection.observeChanges(), this::onItemsChanged); - lifecycleBindable.untilDestroy(observableCollectionSubject - .switchMap(optional -> { - final ObservableCollection collection = optional.get(); - if (collection instanceof ObservableList) { - innerCollection.setDiffUtilsSource((ObservableList) collection); - } else { - innerCollection.setDiffUtilsSource(null); - } - return collection != null ? collection.observeItems() : Observable.just(Collections.emptyList()); - }), innerCollection::set); - lifecycleBindable.untilDestroy(createMoreAutoLoadingObservable()); - } - - @NonNull - private Observable createMoreAutoLoadingObservable() { - return observableCollectionSubject - .switchMap(collectionOptional -> { - final ObservableCollection collection = collectionOptional.get(); - if (!(collection instanceof LoadingMoreList)) { - return Observable.empty(); - } - return moreAutoLoadingRequested - .distinctUntilChanged() - .switchMap(requested -> { - if (!requested) { - return Observable.empty(); - } - final int size = collection.size(); - return ((LoadingMoreList) collection) - .loadRange(size, size + PRE_LOADING_COUNT) - .onErrorResumeNext(Observable.empty()) - .doOnCompleted(() -> moreAutoLoadingRequested.onNext(false)); - }); - }); - } - - /** - * Returns if any change of source collection applied to adapter. - * It's important to not show some footers or headers before first change have applied. - * - * @return True id any change applied. - */ - public boolean isAnyChangeApplied() { - return anyChangeApplied; - } - - @Override - public void onAttachedToRecyclerView(@NonNull final RecyclerView recyclerView) { - super.onAttachedToRecyclerView(recyclerView); - attachedRecyclerViews.add(recyclerView); - } - - private boolean anyRecyclerViewShown() { - for (final RecyclerView recyclerView : attachedRecyclerViews) { - if (recyclerView.isShown()) { - return true; - } - } - return false; - } - - @Override - public void onDetachedFromRecyclerView(@NonNull final RecyclerView recyclerView) { - super.onDetachedFromRecyclerView(recyclerView); - attachedRecyclerViews.remove(recyclerView); - } - - /** - * Returns parent {@link LifecycleBindable} (Activity/ViewController etc.). - * - * @return Parent {@link LifecycleBindable}. - */ - @NonNull - public LifecycleBindable getLifecycleBindable() { - return lifecycleBindable; - } - - /** - * Returns {@link ObservableCollection} which provides items and it's changes. - * - * @return Inner {@link ObservableCollection}. - */ - @Nullable - public ObservableCollection getObservableCollection() { - return observableCollectionSubject.getValue().get(); - } - - /** - * Method to observe {@link ObservableCollection} which provides items and it's changes. - * - * @return Observable of inner {@link ObservableCollection}. - */ - @NonNull - public Observable>> observeObservableCollection() { - return observableCollectionSubject; - } - - /** - * Sets {@link ObservableCollection} which will provide items and it's changes. - * - * @param observableCollection Inner {@link ObservableCollection}. - */ - public void setObservableCollection(@Nullable final ObservableCollection observableCollection) { - this.observableCollectionSubject.onNext(new Optional<>(observableCollection)); - } - - /** - * Simply sets items. - * - * @param items Items to set. - */ - public void setItems(@NonNull final Collection items) { - setObservableCollection(new ObservableList<>(items)); - } - - /** - * Calls when collection changes. - * - * @param collectionChanges Changes of collection. - */ - protected void onItemsChanged(@NonNull final CollectionChanges collectionChanges) { - if (Looper.myLooper() != Looper.getMainLooper()) { - Lc.assertion("Items changes called on not main thread"); - return; - } - if (!anyChangeApplied || !anyRecyclerViewShown()) { - anyChangeApplied = true; - refreshUpdate(); - return; - } - if (collectionChanges.getNumber() != innerCollection.getChangesCount() - || collectionChanges.getNumber() != lastUpdatedChangeNumber + 1) { - if (lastUpdatedChangeNumber < collectionChanges.getNumber()) { - refreshUpdate(); - } - return; - } - notifyAboutChanges(collectionChanges.getChanges()); - lastUpdatedChangeNumber = innerCollection.getChangesCount(); - } - - private void refreshUpdate() { - notifyDataSetChanged(); - lastUpdatedChangeNumber = innerCollection.getChangesCount(); - } - - private void notifyAboutChanges(@NonNull final Collection changes) { - for (final Change change : changes) { - if (change instanceof Change.Inserted) { - final Change.Inserted castedChange = (Change.Inserted) change; - notifyItemRangeInserted(castedChange.getPosition() + getHeadersCount(), castedChange.getCount()); - } else if (change instanceof Change.Removed) { - if (getItemCount() - getHeadersCount() == 0) { - //TODO: bug of recyclerview? - notifyDataSetChanged(); - } else { - final Change.Removed castedChange = (Change.Removed) change; - notifyItemRangeRemoved(castedChange.getPosition() + getHeadersCount(), castedChange.getCount()); - } - } else if (change instanceof Change.Moved) { - final Change.Moved castedChange = (Change.Moved) change; - notifyItemMoved(castedChange.getFromPosition() + getHeadersCount(), castedChange.getToPosition() + getHeadersCount()); - } else if (change instanceof Change.Changed) { - final Change.Changed castedChange = (Change.Changed) change; - notifyItemRangeChanged( - castedChange.getPosition() + getHeadersCount(), - castedChange.getCount(), - castedChange.getPayload()); - } else { - Lc.assertion("Not supported " + change); - } - } - } - - /** - * Returns headers count goes before items. - * - * @return Headers count. - */ - protected int getHeadersCount() { - return 0; - } - - /** - * Returns footers count goes after items and headers. - * - * @return Footers count. - */ - protected int getFootersCount() { - return 0; - } - - /** - * Returns list of added delegates. - * - * @return List of {@link AdapterDelegate}. - */ - @NonNull - public List> getDelegates() { - return Collections.unmodifiableList(delegates); - } - - /** - * Adds {@link ItemAdapterDelegate} to adapter. - * - * @param delegate Delegate to add. - */ - public void addDelegate(@NonNull final ItemAdapterDelegate delegate) { - addDelegateInternal(delegate); - } - - /** - * Adds {@link PositionAdapterDelegate} to adapter. - * - * @param delegate Delegate to add. - */ - public void addDelegate(@NonNull final PositionAdapterDelegate delegate) { - addDelegateInternal(delegate); - } - - private void addDelegateInternal(@NonNull final AdapterDelegate delegate) { - if (inDebugMode) { - for (final AdapterDelegate addedDelegate : delegates) { - if (addedDelegate.getItemViewType() == delegate.getItemViewType()) { - Lc.assertion("AdapterDelegate with viewType=" + delegate.getItemViewType() + " already added"); - return; - } - } - } - delegates.add(delegate); - notifyDataSetChanged(); - } - - /** - * Removes {@link AdapterDelegate} from adapter. - * - * @param delegate Delegate to remove. - */ - public void removeDelegate(@NonNull final AdapterDelegate delegate) { - delegates.remove(delegate); - notifyDataSetChanged(); - } - - private void checkDelegates(@Nullable final AdapterDelegate alreadyPickedDelegate, @NonNull final AdapterDelegate currentDelegate) { - if (alreadyPickedDelegate != null) { - throw new ShouldNotHappenException("Concurrent delegates: " + currentDelegate + " and " + alreadyPickedDelegate); - } - } - - private int getItemPositionInCollection(final int positionInAdapter) { - final int shiftedPosition = positionInAdapter - getHeadersCount(); - return shiftedPosition >= 0 && shiftedPosition < innerCollection.size() ? shiftedPosition : -1; - } - - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", "PMD.StdCyclomaticComplexity", "PMD.NPathComplexity"}) - //Complexity: because of debug code - @Override - public int getItemViewType(final int positionInAdapter) { - AdapterDelegate delegateOfViewType = null; - final int positionInCollection = getItemPositionInCollection(positionInAdapter); - final TItem item = positionInCollection >= 0 ? innerCollection.get(positionInCollection) : null; - for (final AdapterDelegate delegate : delegates) { - if (delegate instanceof ItemAdapterDelegate) { - if (item != null && ((ItemAdapterDelegate) delegate).isForViewType(item, positionInAdapter, positionInCollection)) { - checkDelegates(delegateOfViewType, delegate); - delegateOfViewType = delegate; - if (!inDebugMode) { - break; - } - } - } else if (delegate instanceof PositionAdapterDelegate) { - if (((PositionAdapterDelegate) delegate).isForViewType(positionInAdapter)) { - checkDelegates(delegateOfViewType, delegate); - delegateOfViewType = delegate; - if (!inDebugMode) { - break; - } - } - } else { - Lc.assertion("Delegate of type " + delegate.getClass()); - } - } - - return delegateOfViewType != null ? delegateOfViewType.getItemViewType() : super.getItemViewType(positionInAdapter); - } - - @Override - public long getItemId(final int positionInAdapter) { - final LongContainer result = new LongContainer(); - tryDelegateAction(positionInAdapter, - (itemAdapterDelegate, item, positionInCollection) -> - result.value = itemAdapterDelegate.getItemId(item, positionInAdapter, positionInCollection), - positionAdapterDelegate -> result.value = positionAdapterDelegate.getItemId(positionInAdapter), - (item, positionInCollection) -> result.value = super.getItemId(positionInAdapter)); - return result.value; - } - - private void tryDelegateAction(final int positionInAdapter, - @NonNull final Action3 itemAdapterDelegateAction, - @NonNull final Action1 positionAdapterDelegateAction, - @NonNull final Action2 defaultAction) { - final int viewType = getItemViewType(positionInAdapter); - final int positionInCollection = getItemPositionInCollection(positionInAdapter); - final TItem item = positionInCollection >= 0 ? innerCollection.get(positionInCollection) : null; - for (final AdapterDelegate delegate : delegates) { - if (delegate instanceof ItemAdapterDelegate) { - if (item != null && viewType == delegate.getItemViewType()) { - itemAdapterDelegateAction.call((ItemAdapterDelegate) delegate, item, positionInCollection); - return; - } - } else if (delegate instanceof PositionAdapterDelegate) { - if (viewType == delegate.getItemViewType()) { - positionAdapterDelegateAction.call((PositionAdapterDelegate) delegate); - return; - } - } else { - Lc.assertion("Delegate of type " + delegate.getClass()); - } - } - defaultAction.call(item, positionInCollection); - } - - @Override - public int getItemCount() { - return getHeadersCount() + innerCollection.size() + getFootersCount(); - } - - @NonNull - @Override - public BindableViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { - for (final AdapterDelegate delegate : delegates) { - if (delegate.getItemViewType() == viewType) { - return delegate.onCreateViewHolder(parent); - } - } - throw new ShouldNotHappenException("Add some AdapterDelegate or override this method"); - } - - @Override - public void onBindViewHolder(@NonNull final BindableViewHolder holder, final int positionInAdapter) { - lastUpdatedChangeNumber = innerCollection.getChangesCount(); - - tryDelegateAction(positionInAdapter, - (itemAdapterDelegate, item, positionInCollection) -> { - bindItemViewHolder(itemAdapterDelegate, holder, item, null, positionInAdapter, positionInCollection); - updateMoreAutoLoadingRequest(positionInCollection); - }, - positionAdapterDelegate -> positionAdapterDelegate.onBindViewHolder(holder, positionInAdapter), - (item, positionInCollection) -> { - if (item != null) { - bindItemViewHolder(null, holder, item, null, positionInAdapter, positionInCollection); - } - }); - } - - @Override - public void onBindViewHolder(@NonNull final BindableViewHolder holder, final int positionInAdapter, @NonNull final List payloads) { - super.onBindViewHolder(holder, positionInAdapter, payloads); - tryDelegateAction(positionInAdapter, - (itemAdapterDelegate, item, positionInCollection) -> { - bindItemViewHolder(itemAdapterDelegate, holder, item, payloads, positionInAdapter, positionInCollection); - updateMoreAutoLoadingRequest(positionInCollection); - }, - positionAdapterDelegate -> positionAdapterDelegate.onBindViewHolder(holder, positionInAdapter), - (item, positionInCollection) -> { - if (item != null) { - bindItemViewHolder(null, holder, item, payloads, positionInAdapter, positionInCollection); - } - }); - } - - private void bindItemViewHolder(@Nullable final ItemAdapterDelegate itemAdapterDelegate, - @NonNull final BindableViewHolder holder, @NonNull final TItem item, @Nullable final List payloads, - final int positionInAdapter, final int positionInCollection) { - final TItemViewHolder itemViewHolder; - try { - itemViewHolder = (TItemViewHolder) holder; - } catch (final ClassCastException exception) { - Lc.assertion(exception); - return; - } - updateClickListener(holder, item, positionInAdapter, positionInCollection); - if (itemAdapterDelegate != null) { - if (payloads == null) { - itemAdapterDelegate.onBindViewHolder(itemViewHolder, item, positionInAdapter, positionInCollection); - } else { - itemAdapterDelegate.onBindViewHolder(itemViewHolder, item, payloads, positionInAdapter, positionInCollection); - } - } else { - if (payloads == null) { - onBindItemToViewHolder(itemViewHolder, positionInAdapter, item); - } else { - onBindItemToViewHolder(itemViewHolder, positionInAdapter, item, payloads); - } - } - } - - private void updateClickListener(@NonNull final BindableViewHolder holder, @NonNull final TItem item, - final int positionInAdapter, final int positionInCollection) { - if (onItemClickListener != null && !isOnClickListenerDisabled(item, positionInAdapter, positionInCollection)) { - UiUtils.setOnRippleClickListener(holder.itemView, - () -> { - if (onItemClickListener instanceof OnItemClickListener) { - ((OnItemClickListener) onItemClickListener).onItemClicked(item); - } else if (onItemClickListener instanceof OnItemWithPositionClickListener) { - ((OnItemWithPositionClickListener) onItemClickListener).onItemClicked(item, positionInAdapter, positionInCollection); - } else { - Lc.assertion("Unexpected onItemClickListener type " + onItemClickListener); - } - }, - itemClickDelayMillis); - } - } - - private void updateMoreAutoLoadingRequest(final int positionInCollection) { - if (positionInCollection > innerCollection.size() - PRE_LOADING_COUNT) { - return; - } - moreAutoLoadingRequested.onNext(true); - } - - /** - * Method to bind item (from {@link #getObservableCollection()}) to item-specific ViewHolder. - * It is not calling for headers and footer which counts are returned by {@link #getHeadersCount()} and @link #getFootersCount()}. - * You don't need to override this method if you have delegates for every view type. - * - * @param holder ViewHolder to bind item to; - * @param positionInAdapter Position of ViewHolder (NOT item!); - * @param item Item returned by position (WITH HEADER OFFSET!). - */ - protected void onBindItemToViewHolder(@NonNull final TItemViewHolder holder, final int positionInAdapter, @NonNull final TItem item) { - // do nothing by default - let delegates do it - } - - /** - * Method to bind item (from {@link #getObservableCollection()}) to item-specific ViewHolder with payloads. - * It is not calling for headers and footer which counts are returned by {@link #getHeadersCount()} and @link #getFootersCount()}. - * - * @param holder ViewHolder to bind item to; - * @param positionInAdapter Position of ViewHolder in adapter (NOT item!); - * @param item Item returned by position (WITH HEADER OFFSET!); - * @param payloads Payloads. - */ - protected void onBindItemToViewHolder(@NonNull final TItemViewHolder holder, final int positionInAdapter, @NonNull final TItem item, - @NonNull final List payloads) { - // do nothing by default - let delegates do it - } - - @Nullable - public TItem getItem(final int positionInAdapter) { - final int positionInCollection = getItemPositionInCollection(positionInAdapter); - return positionInCollection >= 0 ? innerCollection.get(positionInCollection) : null; - } - - /** - * Sets item click listener. - * - * @param onItemClickListener Item click listener. - */ - public void setOnItemClickListener(@Nullable final OnItemClickListener onItemClickListener) { - this.setOnItemClickListener(onItemClickListener, UiUtils.RIPPLE_EFFECT_DELAY); - } - - /** - * Sets item click listener. - * - * @param onItemClickListener Item click listener; - * @param itemClickDelayMillis Delay of calling click listener. - */ - public void setOnItemClickListener(@Nullable final OnItemClickListener onItemClickListener, final long itemClickDelayMillis) { - this.onItemClickListener = onItemClickListener; - this.itemClickDelayMillis = itemClickDelayMillis; - refreshUpdate(); - } - - /** - * Sets item click listener. - * - * @param onItemClickListener Item click listener. - */ - public void setOnItemClickListener(@Nullable final OnItemWithPositionClickListener onItemClickListener) { - this.setOnItemClickListener(onItemClickListener, UiUtils.RIPPLE_EFFECT_DELAY); - } - - /** - * Sets item click listener. - * - * @param onItemClickListener Item click listener; - * @param itemClickDelayMillis Delay of calling click listener. - */ - public void setOnItemClickListener(@Nullable final OnItemWithPositionClickListener onItemClickListener, final long itemClickDelayMillis) { - this.onItemClickListener = onItemClickListener; - this.itemClickDelayMillis = itemClickDelayMillis; - refreshUpdate(); - } - - /** - * Returns if click listening disabled or not for specific item. - * - * @param item Item to check click availability; - * @param positionInAdapter Position of clicked item in adapter (with headers); - * @param positionInCollection Position of clicked item in inner collection; - * @return True if click listener enabled for such item. - */ - public boolean isOnClickListenerDisabled(@NonNull final TItem item, final int positionInAdapter, final int positionInCollection) { - return false; - } - - /** - * Enable diff utils algorithm in collection changes. - * - * @param detectMoves The flag that determines whether the {@link Change.Moved} changes will be generated or not; - * @param sameItemsPredicate Predicate for the determination of the same elements; - * @param changePayloadProducer Function that calculate change payload when items the same but contents are different. - */ - public void enableDiffUtils(final boolean detectMoves, - @NonNull final SameItemsPredicate sameItemsPredicate, - @Nullable final ChangePayloadProducer changePayloadProducer) { - innerCollection.enableDiffUtils(detectMoves, sameItemsPredicate, changePayloadProducer); - } - - /** - * Disable diff utils algorithm. - */ - public void disableDiffUtils() { - innerCollection.disableDiffUtils(); - } - - /** - * Returns enabled flag of diff utils. - * - * @return true if diff utils is enabled. - */ - public boolean diffUtilsIsEnabled() { - return innerCollection.diffUtilsIsEnabled(); - } - - /** - * Interface to simply add item click listener. - * - * @param Type of item - */ - public interface OnItemClickListener { - - /** - * Calls when item have clicked. - * - * @param item Clicked item. - */ - void onItemClicked(@NonNull TItem item); - - } - - /** - * Interface to simply add item click listener based on item position in adapter and collection. - * - * @param Type of item - */ - public interface OnItemWithPositionClickListener { - - /** - * Calls when item have clicked. - * - * @param item Clicked item; - * @param positionInAdapter Position of clicked item in adapter (with headers); - * @param positionInCollection Position of clicked item in inner collection. - */ - void onItemClicked(@NonNull TItem item, final int positionInAdapter, final int positionInCollection); - - } - - private class LongContainer { - - private long value; - - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java b/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java deleted file mode 100644 index a3a9f9e..0000000 --- a/src/main/java/ru/touchin/roboswag/components/adapters/PositionAdapterDelegate.java +++ /dev/null @@ -1,69 +0,0 @@ -package ru.touchin.roboswag.components.adapters; - -import android.support.annotation.NonNull; -import android.view.ViewGroup; - -import java.util.List; - -import ru.touchin.roboswag.components.utils.LifecycleBindable; - -/** - * Objects of such class controls creation and binding of specific type of RecyclerView's ViewHolders. - * Such delegates are creating and binding ViewHolders by position in adapter. - * Default {@link #getItemViewType} is generating on construction of object. - * - * @param Type of {@link BindableViewHolder} of delegate. - */ -public abstract class PositionAdapterDelegate extends AdapterDelegate { - - public PositionAdapterDelegate(@NonNull final LifecycleBindable parentLifecycleBindable) { - super(parentLifecycleBindable); - } - - /** - * Returns if object is processable by this delegate. - * - * @param positionInAdapter Position of item in adapter; - * @return True if item is processable by this delegate. - */ - public abstract boolean isForViewType(final int positionInAdapter); - - /** - * Returns unique ID of item to support stable ID's logic of RecyclerView's adapter. - * - * @param positionInAdapter Position of item in adapter; - * @return Unique item ID. - */ - public long getItemId(final int positionInAdapter) { - return 0; - } - - /** - * Creates ViewHolder to bind position to it later. - * - * @param parent Container of ViewHolder's view. - * @return New ViewHolder. - */ - @NonNull - public abstract TViewHolder onCreateViewHolder(@NonNull final ViewGroup parent); - - /** - * Binds position to ViewHolder. - * - * @param holder ViewHolder to bind position to; - * @param positionInAdapter Position of item in adapter. - */ - public abstract void onBindViewHolder(@NonNull final TViewHolder holder, final int positionInAdapter); - - /** - * Binds position with payloads to ViewHolder. - * - * @param holder ViewHolder to bind position to; - * @param payloads Payloads; - * @param positionInAdapter Position of item in adapter. - */ - public void onBindViewHolder(@NonNull final TViewHolder holder, @NonNull final List payloads, final int positionInAdapter) { - //do nothing by default - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/deeplinks/ActivityDeepLinkController.java b/src/main/java/ru/touchin/roboswag/components/deeplinks/ActivityDeepLinkController.java deleted file mode 100644 index 9386c5c..0000000 --- a/src/main/java/ru/touchin/roboswag/components/deeplinks/ActivityDeepLinkController.java +++ /dev/null @@ -1,75 +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.deeplinks; - -import android.app.Activity; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.NonNull; - -import ru.touchin.roboswag.components.navigation.activities.BaseActivity; - - -/** - * Controller that helps to manage deep links in activity. - * It helps to save and restore deep link and deletes deep link info from intent. - * As tin he base class - call methods that starts with 'on' prefix from activity. - * - * @see #onActivityRestoreInstanceState(Bundle) - * @see #onActivitySavedInstanceState(Bundle) - */ -public abstract class ActivityDeepLinkController extends DeepLinkController { - - private static final String DEEP_LINK_EXTRA = "DEEP_LINK_EXTRA"; - - /** - * Call this method on restore instance state - - * in {@link Activity#onCreate(Bundle)} or in {@link Activity#onRestoreInstanceState(Bundle)}. - * - * @param savedInstanceState - activity's savedInstanceState. - */ - public void onActivityRestoreInstanceState(@NonNull final Bundle savedInstanceState) { - final String deepLinkUrl = savedInstanceState.getString(DEEP_LINK_EXTRA, null); - onNewDeepLink(deepLinkUrl == null ? null : Uri.parse(deepLinkUrl)); - } - - /** - * Call this method while saving stat of activity - in {@link Activity#onSaveInstanceState(Bundle)}. - * - * @param stateToSave - activity's stateToSave. - */ - public void onActivitySavedInstanceState(@NonNull final Bundle stateToSave) { - if (getDeepLinkUri() != null) { - stateToSave.putString(DEEP_LINK_EXTRA, getDeepLinkUri().toString()); - } - } - - /** - * Helps to delete info about deep link from activity's intent and from this controller. - * Call this after successful deep link processing. - * - * @param activity - that should delete info about processed deep link. - */ - protected void deleteDeepLink(@NonNull final TActivity activity) { - onNewDeepLink(null); - activity.getIntent().setData(null); - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/deeplinks/DeepLink.java b/src/main/java/ru/touchin/roboswag/components/deeplinks/DeepLink.java deleted file mode 100644 index f1afd2d..0000000 --- a/src/main/java/ru/touchin/roboswag/components/deeplinks/DeepLink.java +++ /dev/null @@ -1,51 +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.deeplinks; - -import android.net.Uri; -import android.support.annotation.NonNull; - -import ru.touchin.roboswag.components.navigation.activities.BaseActivity; - -/** - * Created by Ilia Kurtov on 04.08.2015. - * Class that helps to operate with deep links. - * - * @param Type of Activity to process deep links. - */ -public interface DeepLink { - - /** - * Called by deep link to provide unique name. - */ - @NonNull - String getName(); - - /** - * Called by deep link to decide - whenever deep link should process uri or if we are already on that screen that deep link links to. - */ - boolean isOnSuchScreen(@NonNull TActivity activity, @NonNull Uri deepLinkUri); - - /** - * Called by deep link to navigate to the specific screen. - */ - void navigateTo(@NonNull TActivity activity, @NonNull Uri deepLinkUri); - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/deeplinks/DeepLinkController.java b/src/main/java/ru/touchin/roboswag/components/deeplinks/DeepLinkController.java deleted file mode 100644 index cb8c3ce..0000000 --- a/src/main/java/ru/touchin/roboswag/components/deeplinks/DeepLinkController.java +++ /dev/null @@ -1,123 +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.deeplinks; - -import android.app.Activity; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.touchin.roboswag.components.navigation.activities.BaseActivity; - -/** - * Created by Ilia Kurtov on 04.08.2015. - * Controller for deep links. Its main goal to decide when deep link should be processed. - * Call methods that starts with 'on' prefix from {@link TActivity} that should process deep links. - * - * @param Type of Activity to process deep links. - * @see #onNewDeepLink(Uri) - * @see #onActivityReadyToProcessDeepLink(BaseActivity) - * @see #onActivityStopBeingReady() - */ - -public abstract class DeepLinkController { - - @Nullable - private Uri deepLinkUri; - @Nullable - private TActivity activity; - private boolean allowDeepLinkToProcess = true; - - /** - * Get current deep link. - * - * @return - current deep link - */ - @Nullable - protected Uri getDeepLinkUri() { - return deepLinkUri; - } - - /** - * Call this method after receiving new deep link {@link Uri} from your activity. - * It saves new deepLinkUri and tries to process deep link if possible. - * In most common cases call this method in {@link Activity#onCreate(Bundle)} - * if bundle == null or if you want to restore deep link - * in {@link Activity#onCreate(Bundle)} or in {@link Activity#onRestoreInstanceState(Bundle)} - * methods. - * - * @param deepLinkUri - received deep link. - */ - public void onNewDeepLink(@Nullable final Uri deepLinkUri) { - this.deepLinkUri = deepLinkUri; - startToProcessDeepLinkIfPossible(); - } - - /** - * Call this method when your activity should be ready to process deep link. - * In most common cases call this method on {@link Activity#onStart()} - * - * @param activity - that should be able to process deep link. - */ - public void onActivityReadyToProcessDeepLink(@NonNull final TActivity activity) { - this.activity = activity; - startToProcessDeepLinkIfPossible(); - } - - /** - * Call this method when your activity stopped being ready to process deep link. - * In most common cases call this method on {@link Activity#onStop()} - */ - public void onActivityStopBeingReady() { - activity = null; - } - - /** - * This method should be called when you need to add additional condition - * for processing deep links. By default {@link #allowDeepLinkToProcess} - * equals true. - * - * @param allowDeepLinkToProcess - pass true here if you want to allow deep - * link to process, otherwise - pass false. - */ - public void setAllowDeepLinkToProcess(final boolean allowDeepLinkToProcess) { - this.allowDeepLinkToProcess = allowDeepLinkToProcess; - startToProcessDeepLinkIfPossible(); - } - - private void startToProcessDeepLinkIfPossible() { - if (activity != null && deepLinkUri != null && allowDeepLinkToProcess) { - processDeepLink(activity, deepLinkUri); - } - } - - /** - * This method would be called if there are non null {@link TActivity}, - * non null {@link #deepLinkUri} and {@link #allowDeepLinkToProcess} equals true. - * Don't forget to call activity.getIntent().setData(null) after deep link processing - * - * @param activity - that should be able to process deep link. - * @param deepLinkUri - received deep link. - */ - protected abstract void processDeepLink(@NonNull final TActivity activity, - @NonNull final Uri deepLinkUri); - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/deeplinks/SimpleActivityDeepLinkController.java b/src/main/java/ru/touchin/roboswag/components/deeplinks/SimpleActivityDeepLinkController.java deleted file mode 100644 index 108dbcb..0000000 --- a/src/main/java/ru/touchin/roboswag/components/deeplinks/SimpleActivityDeepLinkController.java +++ /dev/null @@ -1,51 +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.deeplinks; - -import android.net.Uri; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.touchin.roboswag.components.navigation.activities.BaseActivity; - -/** - * Created by Ilia Kurtov on 04.08.2015. - * Simple DeepLinkController that process deep links as it is. When deep links received it would have been processing and navigating id should. - */ -public abstract class SimpleActivityDeepLinkController> - extends ActivityDeepLinkController { - - @Override - protected void processDeepLink(@NonNull final TActivity activity, @NonNull final Uri deepLinkUri) { - deleteDeepLink(activity); - final TDeepLink deepLink = getDeepLinkByUri(deepLinkUri); - if (deepLink != null && !deepLink.isOnSuchScreen(activity, deepLinkUri)) { - deleteDeepLink(activity); - deepLink.navigateTo(activity, deepLinkUri); - } - } - - /** - * Returns deep link that extending {@link DeepLink}. - */ - @Nullable - protected abstract TDeepLink getDeepLinkByUri(@NonNull final Uri deepLinkUri); - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/AbstractState.java b/src/main/java/ru/touchin/roboswag/components/navigation/AbstractState.java deleted file mode 100644 index d747a73..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/AbstractState.java +++ /dev/null @@ -1,52 +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.navigation; - -import android.os.Bundle; -import android.support.v4.app.Fragment; - -import java.io.Serializable; - -/** - * Created by Ilia Kurtov on 13/04/2016. - * Basic state of {@link ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment}. - * This object is saving as serializable in {@link android.os.Bundle} at {@link Fragment#onSaveInstanceState(Bundle)} point. - * Also this object is passing into {@link Fragment#getArguments()} on fragment instantiation. - * Do NOT store such object in fields outside of it's {@link ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment}: - * 1) it should be used as state of fragment but not state of other fragments or parts of logic; - * 2) if you want to modify such object then you should pass it's fragment as {@link Fragment#getTargetFragment()}; - * 3) if you are using {@link ViewControllerNavigation} then just use ***ForResult methods to pass target; - * 4) as it is serializable object then all initialization logic (like binding) should NOT be in constructor. Use {@link #onCreate()} method. - */ -@SuppressWarnings("PMD.AbstractClassWithoutAbstractMethod") -//AbstractClassWithoutAbstractMethod: objects of this class actually shouldn't exist -public abstract class AbstractState implements Serializable { - - private static final long serialVersionUID = 1L; - - /** - * Calls right after construction. All inner object's instantiation logic should be in this method. - * Do NOT do some instantiation logic in constructor except fields setup. - */ - public void onCreate() { - // do nothing - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.java b/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.java deleted file mode 100644 index c73db49..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/FragmentNavigation.java +++ /dev/null @@ -1,419 +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.navigation; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Bundle; -import android.support.annotation.IdRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; -import android.view.MenuItem; - -import ru.touchin.roboswag.core.log.Lc; -import rx.functions.Func1; - -/** - * Created by Gavriil Sitnikov on 07/03/2016. - * Navigation which is controlling fragments on activity using {@link android.support.v4.app.FragmentManager}. - * Basically there are 4 main actions to add fragments to activity. - * 1) {@link #setInitial} means to set fragment on top and remove all previously added fragments from stack; - * 2) {@link #push} means to simply add fragment on top of the stack; - * 3) {@link #setAsTop} means to push fragment on top of the stack with specific {@link #TOP_FRAGMENT_TAG_MARK} tag. - * It is useful to realize up/back navigation: if {@link #up()} method will be called then stack will go to nearest fragment with TOP tag. - * If {@link #back()} method will be called then stack will go to previous fragment. - * Usually such logic using to set as top fragments from sidebar and show hamburger when some of them appeared; - * 4) {@link #pushForResult} means to push fragment with target fragment. It is also adding {@link #WITH_TARGET_FRAGMENT_TAG_MARK} tag. - * Also if such up/back navigation logic is not OK then {@link #backTo(Func1)} method could be used with any condition to back to. - * In that case in any stack-change method it is allowed to setup fragment transactions. - */ -public class FragmentNavigation { - - protected static final String TOP_FRAGMENT_TAG_MARK = "TOP_FRAGMENT"; - protected static final String WITH_TARGET_FRAGMENT_TAG_MARK = "FRAGMENT_WITH_TARGET"; - - @NonNull - private final Context context; - @NonNull - private final FragmentManager fragmentManager; - @IdRes - private final int containerViewId; - - public FragmentNavigation(@NonNull final Context context, @NonNull final FragmentManager fragmentManager, @IdRes final int containerViewId) { - this.context = context; - this.fragmentManager = fragmentManager; - this.containerViewId = containerViewId; - } - - /** - * Returns {@link Context} that is using to instantiate fragments. - * - * @return {@link Context}. - */ - @NonNull - public Context getContext() { - return context; - } - - /** - * Returns {@link FragmentManager} using for navigation. - * - * @return {@link FragmentManager}. - */ - @NonNull - public FragmentManager getFragmentManager() { - return fragmentManager; - } - - /** - * Returns if last fragment in stack is top (added by {@link #setAsTop} or {@link #setInitial}) like fragment from sidebar menu. - * - * @return True if last fragment on stack has TOP_FRAGMENT_TAG_MARK. - */ - public boolean isCurrentFragmentTop() { - if (fragmentManager.getBackStackEntryCount() == 0) { - return true; - } - - final String topFragmentTag = fragmentManager - .getBackStackEntryAt(fragmentManager.getBackStackEntryCount() - 1) - .getName(); - return topFragmentTag != null && topFragmentTag.contains(TOP_FRAGMENT_TAG_MARK); - } - - /** - * Allowed to react on {@link android.app.Activity}'s menu item selection. - * - * @param item Selected menu item; - * @return True if reaction fired. - */ - @SuppressLint("InlinedApi") - //InlinedApi: it is ok as android.R.id.home contains in latest SDK - public boolean onOptionsItemSelected(@NonNull final MenuItem item) { - return item.getItemId() == android.R.id.home && back(); - } - - /** - * Base method which is adding fragment to stack. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param targetFragment Target fragment to be set as {@link Fragment#getTargetFragment()} of instantiated {@link Fragment}; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}; - * @param backStackTag Tag of {@link Fragment} in back stack; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - @SuppressLint("CommitTransaction") - //CommitTransaction: it is ok as we could setup transaction before commit - protected void addToStack(@NonNull final Class fragmentClass, - @Nullable final Fragment targetFragment, - @Nullable final Bundle args, - @Nullable final String backStackTag, - @Nullable final Func1 transactionSetup) { - if (fragmentManager.isDestroyed()) { - Lc.assertion("FragmentManager is destroyed"); - return; - } - - final Fragment fragment = Fragment.instantiate(context, fragmentClass.getName(), args); - if (targetFragment != null) { - if (fragmentManager != targetFragment.getFragmentManager()) { - Lc.assertion("FragmentManager of target is differ then of creating fragment. Target will be lost after restoring activity. " - + targetFragment.getFragmentManager() + " != " + fragmentManager); - } - fragment.setTargetFragment(targetFragment, 0); - } - - final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction() - .replace(containerViewId, fragment, null) - .addToBackStack(backStackTag); - if (fragmentManager.getBackStackEntryCount() != 0) { - fragmentTransaction.setTransition(getDefaultTransition()); - } - if (transactionSetup != null) { - transactionSetup.call(fragmentTransaction).commit(); - } else { - fragmentTransaction.commit(); - } - } - - /** - * Returns default transition animation. - * - * @return {@link FragmentTransaction#TRANSIT_FRAGMENT_OPEN}. - */ - protected int getDefaultTransition() { - return FragmentTransaction.TRANSIT_FRAGMENT_OPEN; - } - - /** - * Simply calls {@link FragmentManager#popBackStack()}. - * - * @return True if it have back to some entry in stack. - */ - public boolean back() { - if (fragmentManager.getBackStackEntryCount() > 1) { - fragmentManager.popBackStack(); - return true; - } - return false; - } - - /** - * Backs to fragment which back stack's entry satisfy to specific condition. - * - * @param condition Condition of back stack entry to be satisfied; - * @return True if it have back to some entry in stack. - */ - public boolean backTo(@NonNull final Func1 condition) { - final int stackSize = fragmentManager.getBackStackEntryCount(); - Integer id = null; - for (int i = stackSize - 2; i >= 0; i--) { - final FragmentManager.BackStackEntry backStackEntry = fragmentManager.getBackStackEntryAt(i); - id = backStackEntry.getId(); - if (condition.call(backStackEntry)) { - break; - } - } - if (id != null) { - fragmentManager.popBackStack(id, 0); - return true; - } - return false; - } - - /** - * Backs to fragment with specific {@link #TOP_FRAGMENT_TAG_MARK} tag. - * This tag is adding if fragment added to stack via {@link #setInitial} or {@link #setAsTop(Class)} methods. - * It can be used to create simple up/back navigation. - * - * @return True if it have back to some entry in stack. - */ - @SuppressWarnings("PMD.ShortMethodName") - //ShortMethodName: it is ok because method name is good! - public boolean up() { - return backTo(backStackEntry -> - backStackEntry.getName() != null && backStackEntry.getName().endsWith(TOP_FRAGMENT_TAG_MARK)); - } - - /** - * Pushes {@link Fragment} on top of stack. - * - * @param fragmentClass Class of {@link Fragment} to instantiate. - */ - public void push(@NonNull final Class fragmentClass) { - addToStack(fragmentClass, null, null, null, null); - } - - /** - * Pushes {@link Fragment} on top of stack with specific arguments. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}. - */ - public void push(@NonNull final Class fragmentClass, - @NonNull final Bundle args) { - addToStack(fragmentClass, null, args, null, null); - } - - /** - * Pushes {@link Fragment} on top of stack with specific transaction setup. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void push(@NonNull final Class fragmentClass, - @NonNull final Func1 transactionSetup) { - addToStack(fragmentClass, null, null, null, transactionSetup); - } - - /** - * Pushes {@link Fragment} on top of stack with specific arguments and transaction setup. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void push(@NonNull final Class fragmentClass, - @Nullable final Bundle args, - @Nullable final Func1 transactionSetup) { - addToStack(fragmentClass, null, args, null, transactionSetup); - } - - /** - * Pushes {@link Fragment} on top of stack with specific target fragment. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param targetFragment Target fragment to be set as {@link Fragment#getTargetFragment()} of instantiated {@link Fragment}. - */ - public void pushForResult(@NonNull final Class fragmentClass, - @NonNull final Fragment targetFragment) { - addToStack(fragmentClass, targetFragment, null, fragmentClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link Fragment} on top of stack with specific target fragment and arguments. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param targetFragment Target fragment to be set as {@link Fragment#getTargetFragment()} of instantiated {@link Fragment}; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}. - */ - public void pushForResult(@NonNull final Class fragmentClass, - @NonNull final Fragment targetFragment, - @NonNull final Bundle args) { - addToStack(fragmentClass, targetFragment, args, fragmentClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link Fragment} on top of stack with specific target fragment and transaction setup. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param targetFragment Target fragment to be set as {@link Fragment#getTargetFragment()} of instantiated {@link Fragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void pushForResult(@NonNull final Class fragmentClass, - @NonNull final Fragment targetFragment, - @NonNull final Func1 transactionSetup) { - addToStack(fragmentClass, targetFragment, null, fragmentClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pushes {@link Fragment} on top of stack with specific target fragment, arguments and transaction setup. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param targetFragment Target fragment to be set as {@link Fragment#getTargetFragment()} of instantiated {@link Fragment}; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void pushForResult(@NonNull final Class fragmentClass, - @NonNull final Fragment targetFragment, - @Nullable final Bundle args, - @Nullable final Func1 transactionSetup) { - addToStack(fragmentClass, targetFragment, args, fragmentClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pushes {@link Fragment} on top of stack with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param fragmentClass Class of {@link Fragment} to instantiate. - */ - public void setAsTop(@NonNull final Class fragmentClass) { - addToStack(fragmentClass, null, null, fragmentClass.getName() + ';' + TOP_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link Fragment} on top of stack with specific arguments and with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}. - */ - public void setAsTop(@NonNull final Class fragmentClass, - @NonNull final Bundle args) { - addToStack(fragmentClass, null, args, fragmentClass.getName() + ';' + TOP_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link Fragment} on top of stack with specific transaction setup - * and with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void setAsTop(@NonNull final Class fragmentClass, - @NonNull final Func1 transactionSetup) { - addToStack(fragmentClass, null, null, fragmentClass.getName() + ';' + TOP_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pushes {@link Fragment} on top of stack with specific transaction setup, arguments - * and with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void setAsTop(@NonNull final Class fragmentClass, - @Nullable final Bundle args, - @Nullable final Func1 transactionSetup) { - addToStack(fragmentClass, null, args, fragmentClass.getName() + ';' + TOP_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link Fragment} on top of stack. - * - * @param fragmentClass Class of {@link Fragment} to instantiate. - */ - public void setInitial(@NonNull final Class fragmentClass) { - setInitial(fragmentClass, null, null); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link Fragment} on top of stack with specific arguments. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}. - */ - public void setInitial(@NonNull final Class fragmentClass, - @NonNull final Bundle args) { - setInitial(fragmentClass, args, null); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link Fragment} on top of stack with specific transaction setup. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void setInitial(@NonNull final Class fragmentClass, - @NonNull final Func1 transactionSetup) { - setInitial(fragmentClass, null, transactionSetup); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link Fragment} on top of stack with specific transaction setup and arguments. - * - * @param fragmentClass Class of {@link Fragment} to instantiate; - * @param args Bundle to be set as {@link Fragment#getArguments()} of instantiated {@link Fragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void setInitial(@NonNull final Class fragmentClass, - @Nullable final Bundle args, - @Nullable final Func1 transactionSetup) { - beforeSetInitialActions(); - setAsTop(fragmentClass, args, transactionSetup); - } - - /** - * Method calls every time before initial {@link Fragment} will be placed. - */ - protected void beforeSetInitialActions() { - if (fragmentManager.isDestroyed()) { - Lc.assertion("FragmentManager is destroyed"); - return; - } - - if (fragmentManager.getBackStackEntryCount() > 0) { - fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); - } - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/ViewController.java b/src/main/java/ru/touchin/roboswag/components/navigation/ViewController.java deleted file mode 100644 index d5f1691..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/ViewController.java +++ /dev/null @@ -1,506 +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.navigation; - -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.annotation.CallSuper; -import android.support.annotation.ColorInt; -import android.support.annotation.ColorRes; -import android.support.annotation.DrawableRes; -import android.support.annotation.IdRes; -import android.support.annotation.LayoutRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.v4.app.Fragment; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; - -import ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity; -import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment; -import ru.touchin.roboswag.components.utils.BaseLifecycleBindable; -import ru.touchin.roboswag.components.utils.LifecycleBindable; -import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; -import rx.Completable; -import rx.Observable; -import rx.Single; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; - -/** - * Created by Gavriil Sitnikov on 21/10/2015. - * Class to control view of specific fragment, activity and application by logic bridge. - * - * @param Type of activity where such {@link ViewController} could be; - * @param Type of fragment where such {@link ViewController} could be; - */ -@SuppressWarnings({"PMD.TooManyMethods", "PMD.ExcessivePublicCount"}) -public class ViewController, - TFragment extends ViewControllerFragment> - implements LifecycleBindable { - - @NonNull - private final TActivity activity; - @NonNull - private final TFragment fragment; - @NonNull - private final ViewGroup container; - @NonNull - private final BaseLifecycleBindable baseLifecycleBindable = new BaseLifecycleBindable(); - private boolean destroyed; - - @SuppressWarnings({"unchecked", "PMD.UnusedFormalParameter"}) - //UnusedFormalParameter: savedInstanceState could be used by children - public ViewController(@NonNull final CreationContext creationContext, @Nullable final Bundle savedInstanceState) { - this.activity = (TActivity) creationContext.activity; - this.fragment = (TFragment) creationContext.fragment; - this.container = creationContext.container; - } - - /** - * Returns activity where {@link ViewController} could be. - * - * @return Returns activity. - */ - @NonNull - public final TActivity getActivity() { - return activity; - } - - /** - * Returns fragment where {@link ViewController} could be. - * - * @return Returns fragment. - */ - @NonNull - public final TFragment getFragment() { - return fragment; - } - - /** - * Returns view instantiated in {@link #getFragment()} fragment attached to {@link #getActivity()} activity. - * Use it to inflate your views into at construction of this {@link ViewController}. - * - * @return Returns view. - */ - @NonNull - public final ViewGroup getContainer() { - return container; - } - - /** - * Returns if {@link ViewController} destroyed or not. - * - * @return True if it is destroyed. - */ - public final boolean isDestroyed() { - return destroyed; - } - - /** - * Return a localized string from the application's package's default string table. - * - * @param resId Resource id for the string - */ - @NonNull - public final String getString(@StringRes final int resId) { - return getActivity().getString(resId); - } - - /** - * Return a localized formatted string from the application's package's default string table, substituting the format arguments as defined in - * {@link java.util.Formatter} and {@link java.lang.String#format}. - * - * @param resId Resource id for the format string - * @param formatArgs The format arguments that will be used for substitution. - */ - @NonNull - public final String getString(@StringRes final int resId, @NonNull final Object... formatArgs) { - return getActivity().getString(resId, formatArgs); - } - - /** - * Set the view controller content from a layout resource. - * This layout is placed directly into the container's ({@link #getContainer()}) view hierarchy. - * - * @param layoutResId Resource ID to be inflated. - */ - public void setContentView(@LayoutRes final int layoutResId) { - if (getContainer().getChildCount() > 0) { - getContainer().removeAllViews(); - } - UiUtils.inflateAndAdd(layoutResId, getContainer()); - } - - /** - * Set the view controller content to an explicit view. - * This view is placed directly into the container's ({@link #getContainer()}) view hierarchy. - * - * @param view The desired content to display. - */ - public void setContentView(@NonNull final View view) { - setContentView(view, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - } - - /** - * Set the view controller content to an explicit view with specific layout parameters. - * This view is placed directly into the container's ({@link #getContainer()}) view hierarchy. - * - * @param view The desired content to display; - * @param layoutParams Layout parameters for the view. - */ - public void setContentView(@NonNull final View view, @NonNull final ViewGroup.LayoutParams layoutParams) { - if (getContainer().getChildCount() > 0) { - getContainer().removeAllViews(); - } - getContainer().addView(view, layoutParams); - } - - /** - * Look for a child view with the given id. If this view has the given id, return this view. - * - * @param id The id to search for; - * @return The view that has the given id in the hierarchy. - */ - @NonNull - @SuppressWarnings("unchecked") - public T findViewById(@IdRes final int id) { - final T viewById = (T) getContainer().findViewById(id); - if (viewById == null) { - throw new ShouldNotHappenException("No view for id=" + getActivity().getResources().getResourceName(id)); - } - return viewById; - } - - /** - * Return the color value associated with a particular resource ID. - * Starting in {@link android.os.Build.VERSION_CODES#M}, the returned - * color will be styled for the specified Context's theme. - * - * @param resId The resource id to search for data; - * @return int A single color value in the form 0xAARRGGBB. - */ - @ColorInt - public int getColor(@ColorRes final int resId) { - return getActivity().getColorCompat(resId); - } - - /** - * Returns a drawable object associated with a particular resource ID. - * Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the - * returned drawable will be styled for the specified Context's theme. - * - * @param resId The resource id to search for data; - * @return Drawable An object that can be used to draw this resource. - */ - @NonNull - public Drawable getDrawable(@DrawableRes final int resId) { - return getActivity().getDrawableCompat(resId); - } - - /** - * Calls when activity configuring ActionBar, Toolbar, Sidebar etc. - * If it will be called or not depends on {@link Fragment#hasOptionsMenu()} and {@link Fragment#isMenuVisible()}. - * - * @param menu The options menu in which you place your items; - * @param inflater Helper to inflate menu items. - */ - public void onConfigureNavigation(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { - // do nothing - } - - /** - * Calls right after construction of {@link ViewController}. - * Happens at {@link ViewControllerFragment#onActivityCreated(View, ViewControllerActivity, Bundle)}. - */ - @CallSuper - public void onCreate() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onCreate(); - } - - /** - * Calls when {@link ViewController} have started. - * Happens at {@link ViewControllerFragment#onStart(View, ViewControllerActivity)}. - */ - @CallSuper - public void onStart() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onStart(); - } - - /** - * Called when fragment is moved in started state and it's {@link #getFragment().isMenuVisible()} sets to true. - * 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)); - } - - /** - * Calls when {@link ViewController} have resumed. - * Happens at {@link ViewControllerFragment#onResume(View, ViewControllerActivity)}. - */ - @CallSuper - public void onResume() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onResume(); - } - - /** - * Calls when {@link ViewController} have goes near out of memory state. - * Happens at {@link ViewControllerFragment#onLowMemory()}. - */ - @CallSuper - public void onLowMemory() { - //do nothing - } - - /** - * Calls when {@link ViewController} have paused. - * Happens at {@link ViewControllerFragment#onPause(View, ViewControllerActivity)}. - */ - @CallSuper - public void onPause() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - } - - /** - * Calls when {@link ViewController} should save it's state. - * Happens at {@link ViewControllerFragment#onSaveInstanceState(Bundle)}. - * Try not to use such method for saving state but use {@link ViewControllerFragment#getState()} from {@link #getFragment()}. - */ - @CallSuper - public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { - baseLifecycleBindable.onSaveInstanceState(); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - } - - /** - * Called when fragment is moved in stopped state or it's {@link #getFragment().isMenuVisible()} sets to false. - * 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)); - } - - /** - * Calls when {@link ViewController} have stopped. - * Happens at {@link ViewControllerFragment#onStop(View, ViewControllerActivity)}. - */ - @CallSuper - public void onStop() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onStop(); - } - - /** - * Calls when {@link ViewController} have destroyed. - * Happens usually at {@link ViewControllerFragment#onDestroyView(View)}. In some cases at {@link ViewControllerFragment#onDestroy()}. - */ - @CallSuper - public void onDestroy() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onDestroy(); - destroyed = true; - } - - /** - * Similar to {@link ViewControllerFragment#onOptionsItemSelected(MenuItem)}. - * - * @param item Selected menu item; - * @return True if selection processed. - */ - public boolean onOptionsItemSelected(@NonNull final MenuItem item) { - return false; - } - - @SuppressWarnings("CPD-START") - //CPD: it is same as in other implementation based on BaseLifecycleBindable - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable) { - return baseLifecycleBindable.untilStop(observable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single) { - return baseLifecycleBindable.untilStop(single); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return baseLifecycleBindable.untilStop(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable) { - return baseLifecycleBindable.untilStop(completable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilStop(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(completable, onCompletedAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable) { - return baseLifecycleBindable.untilDestroy(observable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single) { - return baseLifecycleBindable.untilDestroy(single); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return baseLifecycleBindable.untilDestroy(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable) { - return baseLifecycleBindable.untilDestroy(completable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilDestroy(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(completable, onCompletedAction, onErrorAction); - } - - @SuppressWarnings("CPD-END") - //CPD: it is same as in other implementation based on BaseLifecycleBindable - /** - * Helper class to simplify constructor override. - */ - public static class CreationContext { - - @NonNull - private final ViewControllerActivity activity; - @NonNull - private final ViewControllerFragment fragment; - @NonNull - private final ViewGroup container; - - public CreationContext(@NonNull final ViewControllerActivity activity, - @NonNull final ViewControllerFragment fragment, - @NonNull final ViewGroup container) { - this.activity = activity; - this.fragment = fragment; - this.container = container; - } - - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/ViewControllerNavigation.java b/src/main/java/ru/touchin/roboswag/components/navigation/ViewControllerNavigation.java deleted file mode 100644 index 0690888..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/ViewControllerNavigation.java +++ /dev/null @@ -1,503 +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.navigation; - -import android.content.Context; -import android.support.annotation.IdRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentTransaction; - -import ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity; -import ru.touchin.roboswag.components.navigation.fragments.SimpleViewControllerFragment; -import ru.touchin.roboswag.components.navigation.fragments.StatelessTargetedViewControllerFragment; -import ru.touchin.roboswag.components.navigation.fragments.StatelessViewControllerFragment; -import ru.touchin.roboswag.components.navigation.fragments.TargetedViewControllerFragment; -import ru.touchin.roboswag.components.navigation.fragments.ViewControllerFragment; -import rx.functions.Func1; - -/** - * Created by Gavriil Sitnikov on 07/03/2016. - * Navigation based on {@link ViewController}s which are creating by {@link Fragment}s. - * So basically it is just {@link FragmentNavigation} where most of fragments should be inherited from {@link ViewControllerFragment}. - * - * @param Type of activity where {@link ViewController}s should be showed. - */ -public class ViewControllerNavigation> extends FragmentNavigation { - - public ViewControllerNavigation(@NonNull final Context context, - @NonNull final FragmentManager fragmentManager, - @IdRes final int containerViewId) { - super(context, fragmentManager, containerViewId); - } - - /** - * Pushes {@link ViewControllerFragment} on top of stack. - * - * @param fragmentClass Class of {@link ViewControllerFragment} to instantiate; - * @param state Specific {@link AbstractState} of {@link ViewControllerFragment}; - * @param Type of state of fragment. - */ - public void push(@NonNull final Class> fragmentClass, - @NonNull final TState state) { - addToStack(fragmentClass, null, ViewControllerFragment.createState(state), null, null); - } - - /** - * Pushes {@link ViewControllerFragment} on top of stack with specific transaction setup. - * - * @param fragmentClass Class of {@link ViewControllerFragment} to instantiate; - * @param state Specific {@link AbstractState} of {@link ViewControllerFragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - public void push(@NonNull final Class> fragmentClass, - @Nullable final TState state, - @Nullable final Func1 transactionSetup) { - addToStack(fragmentClass, null, ViewControllerFragment.createState(state), null, transactionSetup); - } - - /** - * Pushes {@link ViewControllerFragment} on top of stack with specific target fragment. - * - * @param fragmentClass Class of {@link ViewControllerFragment} to instantiate; - * @param targetFragment Target fragment to be set as {@link Fragment#getTargetFragment()} of instantiated {@link Fragment}; - * @param state Specific {@link AbstractState} of {@link ViewControllerFragment}; - * @param Type of state of fragment. - */ - public void pushForResult(@NonNull final Class> fragmentClass, - @NonNull final Fragment targetFragment, - @NonNull final TState state) { - addToStack(fragmentClass, targetFragment, ViewControllerFragment.createState(state), - fragmentClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link ViewControllerFragment} on top of stack with specific target fragment and specific transaction setup. - * - * @param fragmentClass Class of {@link ViewControllerFragment} to instantiate; - * @param targetFragment Target fragment to be set as {@link Fragment#getTargetFragment()} of instantiated {@link Fragment}; - * @param state Specific {@link AbstractState} of {@link ViewControllerFragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - public void pushForResult(@NonNull final Class> fragmentClass, - @NonNull final Fragment targetFragment, - @Nullable final TState state, - @Nullable final Func1 transactionSetup) { - addToStack(fragmentClass, targetFragment, ViewControllerFragment.createState(state), - fragmentClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pushes {@link ViewControllerFragment} on top of stack with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param fragmentClass Class of {@link ViewControllerFragment} to instantiate. - * @param state Specific {@link AbstractState} of {@link ViewControllerFragment}; - * @param Type of state of fragment. - */ - public void setAsTop(@NonNull final Class> fragmentClass, - @NonNull final TState state) { - setAsTop(fragmentClass, ViewControllerFragment.createState(state), null); - } - - /** - * Pushes {@link ViewControllerFragment} on top of stack with specific transaction setup - * and with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param fragmentClass Class of {@link ViewControllerFragment} to instantiate. - * @param state Specific {@link AbstractState} of {@link ViewControllerFragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - public void setAsTop(@NonNull final Class> fragmentClass, - @Nullable final TState state, - @Nullable final Func1 transactionSetup) { - setAsTop(fragmentClass, ViewControllerFragment.createState(state), transactionSetup); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link ViewControllerFragment} on top of stack. - * - * @param fragmentClass Class of {@link ViewControllerFragment} to instantiate; - * @param state Specific {@link AbstractState} of {@link ViewControllerFragment}; - * @param Type of state of fragment. - */ - public void setInitial(@NonNull final Class> fragmentClass, - @NonNull final TState state) { - setInitial(fragmentClass, ViewControllerFragment.createState(state), null); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link ViewControllerFragment} on top of stack with specific transaction setup. - * - * @param fragmentClass Class of {@link ViewControllerFragment} to instantiate; - * @param state Specific {@link AbstractState} of {@link ViewControllerFragment}; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - public void setInitial(@NonNull final Class> fragmentClass, - @Nullable final TState state, - @Nullable final Func1 transactionSetup) { - setInitial(fragmentClass, ViewControllerFragment.createState(state), transactionSetup); - } - - /** - * Pushes {@link ViewController} on top of stack. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed. - */ - public void pushViewController(@NonNull final Class>> viewControllerClass) { - addStatelessViewControllerToStack(viewControllerClass, null, null, null); - } - - /** - * Pushes {@link ViewController} on top of stack with specific {@link ViewControllerFragment#getState()}. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param Type of state of fragment. - */ - public void pushViewController(@NonNull final Class>> viewControllerClass, - @NonNull final TState state) { - addViewControllerToStack(viewControllerClass, null, state, null, null); - } - - /** - * Pushes {@link ViewController} on top of stack with specific transaction setup. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info;. - */ - public void pushViewController( - @NonNull final Class>> viewControllerClass, - @Nullable final Func1 transactionSetup) { - addStatelessViewControllerToStack(viewControllerClass, null, null, transactionSetup); - } - - /** - * Pushes {@link ViewController} on top of stack with specific {@link ViewControllerFragment#getState()} and with specific transaction setup. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - public void pushViewController( - @NonNull final Class>> viewControllerClass, - @NonNull final TState state, - @Nullable final Func1 transactionSetup) { - addViewControllerToStack(viewControllerClass, null, state, null, transactionSetup); - } - - /** - * Pushes {@link ViewController} on top of stack with specific {@link StatelessTargetedViewControllerFragment#getState()} - * and with specific {@link TargetedViewControllerFragment#getTarget()}. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param Type of state of target fragment. State is using to affect on that fragment; - * @param Type of target fragment. - */ - @SuppressWarnings("CPD-START") - public > void pushStatelessTargetedViewControllerForResult( - @NonNull final Class>> viewControllerClass, - @NonNull final TTargetFragment targetFragment) { - addToStack(StatelessTargetedViewControllerFragment.class, targetFragment, - StatelessTargetedViewControllerFragment.createState(viewControllerClass), - viewControllerClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link ViewController} on top of stack with specific {@link StatelessTargetedViewControllerFragment#getTarget()}. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param Type of state of target fragment. State is using to affect on that fragment; - * @param Type of target fragment. - */ - @SuppressWarnings("CPD-END") - public > void pushViewControllerForResult( - @NonNull final Class>> viewControllerClass, - @NonNull final TTargetFragment targetFragment) { - addTargetedStatelessViewControllerToStack(viewControllerClass, targetFragment, - viewControllerClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link ViewController} on top of stack with specific {@link StatelessTargetedViewControllerFragment#getTarget()} and transaction setup. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of target fragment. State is using to affect on that fragment; - * @param Type of target fragment. - */ - public > void pushViewControllerForResult( - @NonNull final Class>> viewControllerClass, - @NonNull final TTargetFragment targetFragment, - @Nullable final Func1 transactionSetup) { - addTargetedStatelessViewControllerToStack(viewControllerClass, targetFragment, - viewControllerClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pushes {@link ViewController} on top of stack with specific with specific {@link ViewControllerFragment#getState()} - * and with specific {@link TargetedViewControllerFragment#getTarget()}. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param Type of state of fragment; - * @param Type of state of target fragment. State is using to affect on that fragment; - * @param Type of target fragment. - */ - @SuppressWarnings("CPD-START") - public > void pushViewControllerForResult( - @NonNull final Class>> viewControllerClass, - @NonNull final TTargetFragment targetFragment, - @NonNull final TState state) { - addTargetedViewControllerToStack(viewControllerClass, targetFragment, state, - viewControllerClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link ViewController} on top of stack with specific {@link ViewControllerFragment#getState()} - * and with specific {@link TargetedViewControllerFragment#getTarget()} and transaction setup. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment; - * @param Type of state of target fragment. State is using to affect on that fragment; - * @param Type of target fragment. - */ - @SuppressWarnings("CPD-END") - public > void pushViewControllerForResult( - @NonNull final Class>> viewControllerClass, - @NonNull final TTargetFragment targetFragment, - @NonNull final TState state, - @Nullable final Func1 transactionSetup) { - addTargetedViewControllerToStack(viewControllerClass, targetFragment, state, - viewControllerClass.getName() + ';' + WITH_TARGET_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pushes {@link ViewController} on top of stack with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed. - */ - public void setViewControllerAsTop( - @NonNull final Class>> viewControllerClass) { - addStatelessViewControllerToStack(viewControllerClass, null, viewControllerClass.getName() + ' ' + TOP_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link ViewController} on top of stack with specific {@link ViewControllerFragment#getState()} - * and with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param Type of state of fragment. - */ - public void setViewControllerAsTop( - @NonNull final Class>> viewControllerClass, - @NonNull final TState state) { - addViewControllerToStack(viewControllerClass, null, state, viewControllerClass.getName() + ' ' + TOP_FRAGMENT_TAG_MARK, null); - } - - /** - * Pushes {@link ViewController} on top of stack with specific transaction setup - * and with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - public void setViewControllerAsTop( - @NonNull final Class>> viewControllerClass, - @Nullable final Func1 transactionSetup) { - addStatelessViewControllerToStack(viewControllerClass, null, viewControllerClass.getName() + ' ' + TOP_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pushes {@link ViewController} on top of stack with specific {@link ViewControllerFragment#getState()} and with specific transaction setup - * and with {@link #TOP_FRAGMENT_TAG_MARK} tag used for simple up/back navigation. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - public void setViewControllerAsTop( - @NonNull final Class>> viewControllerClass, - @NonNull final TState state, - @Nullable final Func1 transactionSetup) { - addViewControllerToStack(viewControllerClass, null, state, viewControllerClass.getName() + ' ' + TOP_FRAGMENT_TAG_MARK, transactionSetup); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link ViewController} on top of stack. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - */ - public void setInitialViewController( - @NonNull final Class>> viewControllerClass) { - beforeSetInitialActions(); - setViewControllerAsTop(viewControllerClass); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link ViewController} on top of stack - * with specific {@link ViewControllerFragment#getState()}. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param Type of state of fragment. - */ - public void setInitialViewController( - @NonNull final Class>> viewControllerClass, - @NonNull final TState state) { - setInitialViewController(viewControllerClass, state, null); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link ViewController} on top of stack with specific transaction setup. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - */ - public void setInitialViewController( - @NonNull final Class>> viewControllerClass, - @Nullable final Func1 transactionSetup) { - beforeSetInitialActions(); - setViewControllerAsTop(viewControllerClass, transactionSetup); - } - - /** - * Pops all {@link Fragment}s and places new initial {@link ViewController} on top of stack - * with specific {@link ViewControllerFragment#getState()} and specific transaction setup. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - public void setInitialViewController( - @NonNull final Class>> viewControllerClass, - @NonNull final TState state, - @Nullable final Func1 transactionSetup) { - beforeSetInitialActions(); - setViewControllerAsTop(viewControllerClass, state, transactionSetup); - } - - /** - * Base method to push stateless {@link ViewControllerFragment} to stack. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param backStackTag Tag of {@link ViewControllerFragment} in back stack; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info. - */ - protected void addStatelessViewControllerToStack( - @NonNull final Class>> viewControllerClass, - @Nullable final Fragment targetFragment, - @Nullable final String backStackTag, - @Nullable final Func1 transactionSetup) { - addToStack(StatelessViewControllerFragment.class, targetFragment, - StatelessViewControllerFragment.createState(viewControllerClass), backStackTag, transactionSetup); - } - - /** - * Base method to push stateful {@link ViewControllerFragment} with target to stack. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param backStackTag Tag of {@link ViewControllerFragment} in back stack; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - * @param Type of state of target fragment. State is using to affect on that fragment; - */ - protected void addTargetedViewControllerToStack( - @NonNull final Class>> viewControllerClass, - @NonNull final Fragment targetFragment, - @NonNull final TState state, - @Nullable final String backStackTag, - @Nullable final Func1 transactionSetup) { - addToStack(TargetedViewControllerFragment.class, targetFragment, - TargetedViewControllerFragment.createState(viewControllerClass, state), backStackTag, transactionSetup); - } - - /** - * Base method to push stateless {@link ViewControllerFragment} with target to stack. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param backStackTag Tag of {@link ViewControllerFragment} in back stack; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - protected void addTargetedStatelessViewControllerToStack( - @NonNull final Class>> viewControllerClass, - @NonNull final Fragment targetFragment, - @Nullable final String backStackTag, - @Nullable final Func1 transactionSetup) { - addToStack(StatelessTargetedViewControllerFragment.class, targetFragment, - StatelessTargetedViewControllerFragment.createState(viewControllerClass), backStackTag, transactionSetup); - } - - /** - * Base method to push stateful {@link ViewControllerFragment} to stack. - * - * @param viewControllerClass Class of {@link ViewController} to be pushed; - * @param targetFragment {@link ViewControllerFragment} to be set as target; - * @param state {@link AbstractState} of {@link ViewController}'s fragment; - * @param backStackTag Tag of {@link ViewControllerFragment} in back stack; - * @param transactionSetup Function to setup transaction before commit. It is useful to specify transition animations or additional info; - * @param Type of state of fragment. - */ - protected void addViewControllerToStack( - @NonNull final Class>> viewControllerClass, - @Nullable final Fragment targetFragment, - @NonNull final TState state, - @Nullable final String backStackTag, - @Nullable final Func1 transactionSetup) { - addToStack(SimpleViewControllerFragment.class, targetFragment, - SimpleViewControllerFragment.createState(viewControllerClass, state), backStackTag, transactionSetup); - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java b/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java deleted file mode 100644 index 700bd5e..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/activities/BaseActivity.java +++ /dev/null @@ -1,414 +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.navigation.activities; - -import android.app.Activity; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.annotation.ColorInt; -import android.support.annotation.ColorRes; -import android.support.annotation.DrawableRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.content.ContextCompat; -import android.support.v7.app.AppCompatActivity; -import android.view.View; -import android.view.inputmethod.InputMethodManager; - -import java.util.ArrayList; - -import ru.touchin.roboswag.components.utils.BaseLifecycleBindable; -import ru.touchin.roboswag.components.utils.LifecycleBindable; -import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.Optional; -import ru.touchin.roboswag.core.utils.pairs.HalfNullablePair; -import rx.Completable; -import rx.Observable; -import rx.Single; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.subjects.BehaviorSubject; - -/** - * Created by Gavriil Sitnikov on 08/03/2016. - * Base activity to use in components repository. - */ -@SuppressWarnings("PMD.TooManyMethods") -public abstract class BaseActivity extends AppCompatActivity - implements LifecycleBindable { - - private static final String ACTIVITY_RESULT_CODE_EXTRA = "ACTIVITY_RESULT_CODE_EXTRA"; - private static final String ACTIVITY_RESULT_DATA_EXTRA = "ACTIVITY_RESULT_DATA_EXTRA"; - - @NonNull - private final ArrayList onBackPressedListeners = new ArrayList<>(); - @NonNull - private final BaseLifecycleBindable baseLifecycleBindable = new BaseLifecycleBindable(); - private boolean resumed; - - @NonNull - private final BehaviorSubject>> lastActivityResult - = BehaviorSubject.create(new Optional>(null)); - - /** - * Returns if activity resumed. - * - * @return True if resumed. - */ - public boolean isActuallyResumed() { - return resumed; - } - - @Override - protected void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onCreate(); - restoreLastActivityResult(savedInstanceState); - } - - private void restoreLastActivityResult(@Nullable final Bundle savedInstanceState) { - if (savedInstanceState == null) { - return; - } - - lastActivityResult.onNext(new Optional<>(new HalfNullablePair<>(savedInstanceState.getInt(ACTIVITY_RESULT_CODE_EXTRA), - savedInstanceState.getParcelable(ACTIVITY_RESULT_DATA_EXTRA)))); - } - - @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); - if (resultCode == RESULT_OK) { - lastActivityResult.onNext(new Optional<>(new HalfNullablePair<>(requestCode, data))); - } - } - - /** - * Observes activity result by request code coming from {@link #onActivityResult(int, int, Intent)} - * - * @param requestCode Unique code to identify activity result; - * @return {@link Observable} which will emit data (Intents) from other activities (endlessly). - */ - @NonNull - public Observable observeActivityResult(final int requestCode) { - return lastActivityResult - .concatMap(optional -> { - final HalfNullablePair activityResult = optional.get(); - if (activityResult == null || activityResult.getFirst() != requestCode) { - return Observable.empty(); - } - return Observable.just(activityResult.getSecond() != null ? activityResult.getSecond() : new Intent()) - .doOnNext(result -> lastActivityResult.onNext(new Optional<>(null))); - }); - } - - @Override - protected void onStart() { - super.onStart(); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onStart(); - } - - @Override - protected void onResume() { - super.onResume(); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - resumed = true; - baseLifecycleBindable.onResume(); - } - - @Override - protected void onPause() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - resumed = false; - super.onPause(); - } - - @Override - protected void onSaveInstanceState(@NonNull final Bundle stateToSave) { - super.onSaveInstanceState(stateToSave); - baseLifecycleBindable.onSaveInstanceState(); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - final HalfNullablePair activityResult = lastActivityResult.getValue().get(); - if (activityResult != null) { - stateToSave.putInt(ACTIVITY_RESULT_CODE_EXTRA, activityResult.getFirst()); - if (activityResult.getSecond() != null) { - stateToSave.putParcelable(ACTIVITY_RESULT_DATA_EXTRA, activityResult.getSecond()); - } - } - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - } - - @Override - protected void onStop() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onStop(); - super.onStop(); - } - - @Override - protected void onDestroy() { - UiUtils.UI_LIFECYCLE_LC_GROUP.i(Lc.getCodePoint(this)); - baseLifecycleBindable.onDestroy(); - super.onDestroy(); - } - - /** - * Hides device keyboard that is showing over {@link Activity}. - * Do NOT use it if keyboard is over {@link android.app.Dialog} - it won't work as they have different {@link Activity#getWindow()}. - */ - public void hideSoftInput() { - if (getCurrentFocus() == null) { - return; - } - final InputMethodManager inputManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); - getWindow().getDecorView().requestFocus(); - } - - /** - * Shows device keyboard over {@link Activity} and focuses {@link View}. - * Do NOT use it if keyboard is over {@link android.app.Dialog} - it won't work as they have different {@link Activity#getWindow()}. - * Do NOT use it if you are not sure that view is already added on screen. - * Better use it onStart of element if view is part of it or onConfigureNavigation if view is part of navigation. - * - * @param view View to get focus for input from keyboard. - */ - public void showSoftInput(@NonNull final View view) { - view.requestFocus(); - final InputMethodManager inputManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); - } - - /** - * Return the color value associated with a particular resource ID. - * Starting in {@link android.os.Build.VERSION_CODES#M}, the returned - * color will be styled for the specified Context's theme. - * - * @param resId The resource id to search for data; - * @return int A single color value in the form 0xAARRGGBB. - */ - @ColorInt - public int getColorCompat(@ColorRes final int resId) { - return ContextCompat.getColor(this, resId); - } - - /** - * Returns a drawable object associated with a particular resource ID. - * Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the - * returned drawable will be styled for the specified Context's theme. - * - * @param resId The resource id to search for data; - * @return Drawable An object that can be used to draw this resource. - */ - @NonNull - public Drawable getDrawableCompat(@DrawableRes final int resId) { - return ContextCompat.getDrawable(this, resId); - } - - public void addOnBackPressedListener(@NonNull final OnBackPressedListener onBackPressedListener) { - onBackPressedListeners.add(onBackPressedListener); - } - - public void removeOnBackPressedListener(@NonNull final OnBackPressedListener onBackPressedListener) { - onBackPressedListeners.remove(onBackPressedListener); - } - - @Override - public void onBackPressed() { - for (final OnBackPressedListener onBackPressedListener : onBackPressedListeners) { - if (onBackPressedListener.onBackPressed()) { - return; - } - } - - if (getSupportFragmentManager().getBackStackEntryCount() <= 1) { - supportFinishAfterTransition(); - } else { - getSupportFragmentManager().popBackStack(); - } - } - - @SuppressWarnings("CPD-START") - //CPD: it is same as in other implementation based on BaseLifecycleBindable - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single) { - return baseLifecycleBindable.untilStop(single); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return baseLifecycleBindable.untilStop(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable) { - return baseLifecycleBindable.untilStop(completable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilStop(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(completable, onCompletedAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable) { - return baseLifecycleBindable.untilStop(observable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable) { - return baseLifecycleBindable.untilDestroy(observable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single) { - return baseLifecycleBindable.untilDestroy(single); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return baseLifecycleBindable.untilDestroy(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable) { - return baseLifecycleBindable.untilDestroy(completable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilDestroy(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(completable, onCompletedAction, onErrorAction); - } - - @SuppressWarnings("CPD-END") - /* - * Interface to be implemented for someone who want to intercept device back button pressing event. - */ - public interface OnBackPressedListener { - - /** - * Calls when user presses device back button. - * - * @return True if it is processed by this object. - */ - boolean onBackPressed(); - - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/activities/ViewControllerActivity.java b/src/main/java/ru/touchin/roboswag/components/navigation/activities/ViewControllerActivity.java deleted file mode 100644 index b49d1a8..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/activities/ViewControllerActivity.java +++ /dev/null @@ -1,114 +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.navigation.activities; - -import android.support.annotation.IdRes; -import android.support.annotation.NonNull; -import android.view.Menu; -import android.view.View; - -import ru.touchin.roboswag.components.utils.Logic; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; - -/** - * Created by Gavriil Sitnikov on 07/03/2016. - * Activity which is containing specific {@link Logic} - * to support navigation based on {@link ru.touchin.roboswag.components.navigation.ViewController}s. - * - * @param Type of application's {@link Logic}. - */ -public abstract class ViewControllerActivity extends BaseActivity { - - //it is needed to hold strong reference to logic - private TLogic reference; - - /** - * It should return specific class where all logic will be. - * - * @return Returns class of specific {@link Logic}. - */ - @NonNull - protected abstract Class getLogicClass(); - - /** - * Returns (and creates if needed) application's logic. - * - * @return Object which represents application's logic. - */ - @NonNull - public TLogic getLogic() { - synchronized (ViewControllerActivity.class) { - if (reference == null) { - reference = Logic.getInstance(this, getLogicClass()); - } - } - return reference; - } - - @Override - @Deprecated - // use {@link #reconfigureNavigation} - public void invalidateOptionsMenu() { - super.invalidateOptionsMenu(); - } - - @Override - @Deprecated - // use {@link #reconfigureNavigation} - public void supportInvalidateOptionsMenu() { - super.supportInvalidateOptionsMenu(); - } - - /** - * Invalidates navigation and calls {@link #onConfigureNavigation} for all navigation elements. - */ - public void reconfigureNavigation() { - super.supportInvalidateOptionsMenu(); - } - - @Override - @Deprecated - // use {@link #onConfigureNavigation} - public boolean onCreateOptionsMenu(@NonNull final Menu menu) { - onConfigureNavigation(menu); - return super.onCreateOptionsMenu(menu); - } - - /** - * Calls when activity configuring ActionBar, Toolbar, Sidebar, AppBar etc. - * It is calling before it's {@link ru.touchin.roboswag.components.navigation.ViewController}'s. - * - * @param menu The options menu in which you place your menu items. - */ - public void onConfigureNavigation(@NonNull final Menu menu) { - // do nothing - } - - @NonNull - @Override - public View findViewById(@IdRes final int id) { - final View viewById = super.findViewById(id); - if (viewById == null) { - throw new ShouldNotHappenException("No view for id=" + getResources().getResourceName(id)); - } - return viewById; - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/SimpleViewControllerFragment.java b/src/main/java/ru/touchin/roboswag/components/navigation/fragments/SimpleViewControllerFragment.java deleted file mode 100644 index 900804d..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/SimpleViewControllerFragment.java +++ /dev/null @@ -1,84 +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.navigation.fragments; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import ru.touchin.roboswag.components.navigation.AbstractState; -import ru.touchin.roboswag.components.navigation.ViewController; -import ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity; - -/** - * Created by Gavriil Sitnikov on 07/03/2016. - * Simple {@link ViewControllerFragment} which is using by {@link ru.touchin.roboswag.components.navigation.ViewControllerNavigation}. - * - * @param Type of object which is representing it's fragment state; - * @param Type of {@link ViewControllerActivity} where fragment could be attached to. - */ -public class SimpleViewControllerFragment> - extends ViewControllerFragment { - - private static final String VIEW_CONTROLLER_CLASS_EXTRA = "VIEW_CONTROLLER_CLASS_EXTRA"; - - /** - * Creates {@link Bundle} which will store state and {@link ViewController}'s class. - * - * @param viewControllerClass Class of {@link ViewController} which will be instantiated inside this fragment; - * @param state State to use into {@link ViewController}; - * @return Returns {@link Bundle} with state inside. - */ - @NonNull - public static Bundle createState(@NonNull final Class viewControllerClass, - @NonNull final AbstractState state) { - final Bundle result = createState(state); - result.putSerializable(VIEW_CONTROLLER_CLASS_EXTRA, viewControllerClass); - return result; - } - - private Class>> viewControllerClass; - - @NonNull - @Override - public Class>> getViewControllerClass() { - return viewControllerClass; - } - - @Override - protected boolean isStateRequired() { - return true; - } - - @SuppressWarnings("unchecked") - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - viewControllerClass = (Class>>) getArguments().getSerializable(VIEW_CONTROLLER_CLASS_EXTRA); - } - - protected static class DefaultState extends AbstractState { - // just default implementation - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/StatelessTargetedViewControllerFragment.java b/src/main/java/ru/touchin/roboswag/components/navigation/fragments/StatelessTargetedViewControllerFragment.java deleted file mode 100644 index bc5421a..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/StatelessTargetedViewControllerFragment.java +++ /dev/null @@ -1,66 +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.navigation.fragments; - -import android.os.Bundle; -import android.support.annotation.NonNull; - -import ru.touchin.roboswag.components.navigation.AbstractState; -import ru.touchin.roboswag.components.navigation.ViewController; -import ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity; -import ru.touchin.roboswag.core.log.Lc; - -/** - * Created by Gavriil Sitnikov on 11/04/2016. - * Simple {@link ViewControllerFragment} with no state and with attached {@link #getTargetFragment()} - * which is using by {@link ru.touchin.roboswag.components.navigation.ViewControllerNavigation}. - * - * @param Type of {@link ViewControllerActivity} where fragment could be attached to. - */ -@SuppressWarnings("PMD.UseUtilityClass") -//UseUtilityClass: PMD bug -public class StatelessTargetedViewControllerFragment> - extends TargetedViewControllerFragment { - - /** - * Creates {@link Bundle} which will store state and {@link ViewController}'s class. - * - * @param viewControllerClass Class of {@link ViewController} which will be instantiated inside this fragment; - * @return Returns {@link Bundle} with state inside. - */ - @NonNull - public static Bundle createState(@NonNull final Class viewControllerClass) { - return createState(viewControllerClass, new DefaultState()); - } - - @Override - protected boolean isStateRequired() { - return false; - } - - @NonNull - @Override - public AbstractState getState() { - Lc.assertion("Trying to access to state of stateless fragment of " + getViewControllerClass()); - return super.getState(); - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/StatelessViewControllerFragment.java b/src/main/java/ru/touchin/roboswag/components/navigation/fragments/StatelessViewControllerFragment.java deleted file mode 100644 index f463ae6..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/StatelessViewControllerFragment.java +++ /dev/null @@ -1,64 +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.navigation.fragments; - -import android.os.Bundle; -import android.support.annotation.NonNull; - -import ru.touchin.roboswag.components.navigation.AbstractState; -import ru.touchin.roboswag.components.navigation.ViewController; -import ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity; -import ru.touchin.roboswag.core.log.Lc; - -/** - * Created by Gavriil Sitnikov on 12/03/2016. - * Simple {@link ViewControllerFragment} with no state which is using by {@link ru.touchin.roboswag.components.navigation.ViewControllerNavigation}. - * - * @param Type of {@link ViewControllerActivity} where fragment could be attached to. - */ -@SuppressWarnings("PMD.UseUtilityClass") -//UseUtilityClass: PMD bug -public class StatelessViewControllerFragment> - extends SimpleViewControllerFragment { - - /** - * Creates {@link Bundle} which will store state and {@link ViewController}'s class. - * - * @param viewControllerClass Class of {@link ViewController} which will be instantiated inside this fragment; - * @return Returns {@link Bundle} with state inside. - */ - @NonNull - public static Bundle createState(@NonNull final Class viewControllerClass) { - return createState(viewControllerClass, new DefaultState()); - } - - @NonNull - @Override - public AbstractState getState() { - Lc.assertion("Trying to access to state of stateless fragment of " + getViewControllerClass()); - return super.getState(); - } - - @Override - protected boolean isStateRequired() { - return false; - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/TargetedViewControllerFragment.java b/src/main/java/ru/touchin/roboswag/components/navigation/fragments/TargetedViewControllerFragment.java deleted file mode 100644 index 5f5f676..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/TargetedViewControllerFragment.java +++ /dev/null @@ -1,55 +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.navigation.fragments; - -import android.support.annotation.NonNull; - -import ru.touchin.roboswag.components.navigation.AbstractState; -import ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; - -/** - * Created by Gavriil Sitnikov on 11/04/2016. - * Simple {@link ViewControllerFragment} with attached {@link #getTargetFragment()} - * which is using by {@link ru.touchin.roboswag.components.navigation.ViewControllerNavigation}. - * - * @param Type of object which is representing it's fragment state; - * @param Type of {@link ViewControllerActivity} where fragment could be attached to. - */ -public class TargetedViewControllerFragment> - extends SimpleViewControllerFragment { - - /** - * Returns specific {@link ViewControllerFragment} which is attached to this fragment as {@link #getTargetFragment()}. - * - * @return Target fragment. - */ - @SuppressWarnings("unchecked") - @NonNull - public ViewControllerFragment getTarget() { - if (!(getTargetFragment() instanceof ViewControllerFragment)) { - throw new ShouldNotHappenException(); - } - return (ViewControllerFragment) getTargetFragment(); - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java b/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java deleted file mode 100644 index 260c315..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewControllerFragment.java +++ /dev/null @@ -1,426 +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.navigation.fragments; - -import android.content.Context; -import android.graphics.Canvas; -import android.os.Bundle; -import android.os.Parcel; -import android.os.SystemClock; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.view.InflateException; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import java.io.Serializable; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -import ru.touchin.roboswag.components.navigation.AbstractState; -import ru.touchin.roboswag.components.navigation.ViewController; -import ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity; -import ru.touchin.roboswag.components.utils.UiUtils; -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.Optional; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; -import ru.touchin.roboswag.core.utils.pairs.NullablePair; -import rx.Observable; -import rx.Subscription; -import rx.exceptions.OnErrorThrowable; -import rx.subjects.BehaviorSubject; - -/** - * Created by Gavriil Sitnikov on 21/10/2015. - * Fragment instantiated in specific activity of {@link TActivity} type that is holding {@link ViewController} inside. - * - * @param Type of object which is representing it's fragment state; - * @param Type of {@link ViewControllerActivity} where fragment could be attached to. - */ -@SuppressWarnings("PMD.TooManyMethods") -public abstract class ViewControllerFragment> - extends ViewFragment { - - private static final String VIEW_CONTROLLER_STATE_EXTRA = "VIEW_CONTROLLER_STATE_EXTRA"; - - private static boolean inDebugMode; - private static long acceptableUiCalculationTime = 100; - - /** - * Enables debugging features like serialization of {@link #getState()} every creation. - */ - public static void setInDebugMode() { - inDebugMode = true; - } - - /** - * Sets acceptable UI calculation time so there will be warnings in logs if ViewController's inflate/layout actions will take more than that time. - * Works only if {@link #setInDebugMode()} called. - * It's 100ms by default. - */ - public static void setAcceptableUiCalculationTime(final long acceptableUiCalculationTime) { - ViewControllerFragment.acceptableUiCalculationTime = acceptableUiCalculationTime; - } - - @SuppressWarnings("unchecked") - @NonNull - private static T reserialize(@NonNull final T serializable) { - Parcel parcel = Parcel.obtain(); - parcel.writeSerializable(serializable); - final byte[] serializableBytes = parcel.marshall(); - parcel.recycle(); - parcel = Parcel.obtain(); - parcel.unmarshall(serializableBytes, 0, serializableBytes.length); - parcel.setDataPosition(0); - final T result = (T) parcel.readSerializable(); - parcel.recycle(); - return result; - } - - /** - * Creates {@link Bundle} which will store state. - * - * @param state State to use into ViewController. - * @return Returns bundle with state inside. - */ - @NonNull - public static Bundle createState(@Nullable final AbstractState state) { - final Bundle result = new Bundle(); - result.putSerializable(VIEW_CONTROLLER_STATE_EXTRA, state); - return result; - } - - @NonNull - private final BehaviorSubject> activitySubject = BehaviorSubject.create(); - @NonNull - private final BehaviorSubject> viewSubject = BehaviorSubject.create(); - @Nullable - private ViewController viewController; - private Subscription viewControllerSubscription; - private TState state; - private boolean started; - private boolean stateCreated; - - private void tryCreateState(@Nullable final Context context) { - if (!stateCreated && state != null && context != null) { - state.onCreate(); - stateCreated = true; - } - } - - /** - * Returns specific {@link AbstractState} which contains state of fragment and it's {@link ViewController}. - * - * @return Object represents state. - */ - @NonNull - public TState getState() { - return state; - } - - /** - * It should return specific {@link ViewController} class to control instantiated view by logic after activity creation. - * - * @return Returns class of specific {@link ViewController}. - */ - @NonNull - public abstract Class>> getViewControllerClass(); - - /** - * Returns if ViewControllerFragment requires state or not. - * - * @return true if state is required - */ - protected abstract boolean isStateRequired(); - - @SuppressWarnings("unchecked") - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setHasOptionsMenu(!isChildFragment()); - - state = savedInstanceState != null - ? (TState) savedInstanceState.getSerializable(VIEW_CONTROLLER_STATE_EXTRA) - : (getArguments() != null ? (TState) getArguments().getSerializable(VIEW_CONTROLLER_STATE_EXTRA) : null); - if (state != null) { - if (inDebugMode) { - state = reserialize(state); - } - tryCreateState(getContext()); - } else if (isStateRequired()) { - Lc.assertion("State is required and null"); - } - viewControllerSubscription = Observable - .combineLatest(activitySubject.distinctUntilChanged(), viewSubject.distinctUntilChanged(), - (activityOptional, viewInfo) -> { - final TActivity activity = activityOptional.get(); - final PlaceholderView container = viewInfo.getFirst(); - if (activity == null || container == null) { - return null; - } - final ViewController newViewController = createViewController(activity, container, viewInfo.getSecond()); - newViewController.onCreate(); - return newViewController; - }) - .subscribe(this::onViewControllerChanged, - throwable -> Lc.cutAssertion(throwable, - OnErrorThrowable.class, InvocationTargetException.class, InflateException.class)); - } - - @NonNull - private ViewController createViewController(@NonNull final TActivity activity, @NonNull final PlaceholderView view, - @Nullable final Bundle savedInstanceState) { - - if (getViewControllerClass().getConstructors().length != 1) { - throw OnErrorThrowable.from(new ShouldNotHappenException("There should be single constructor for " + getViewControllerClass())); - } - final Constructor constructor = getViewControllerClass().getConstructors()[0]; - final ViewController.CreationContext creationContext = new ViewController.CreationContext(activity, this, view); - final long creationTime = inDebugMode ? SystemClock.elapsedRealtime() : 0; - try { - switch (constructor.getParameterTypes().length) { - case 2: - return (ViewController) constructor.newInstance(creationContext, savedInstanceState); - case 3: - return (ViewController) constructor.newInstance(this, creationContext, savedInstanceState); - default: - throw OnErrorThrowable - .from(new ShouldNotHappenException("Wrong constructor parameters count: " + constructor.getParameterTypes().length)); - } - } catch (final Exception exception) { - throw OnErrorThrowable.from(exception); - } finally { - checkCreationTime(creationTime); - } - } - - private void checkCreationTime(final long creationTime) { - if (inDebugMode) { - final long creationPeriod = SystemClock.elapsedRealtime() - creationTime; - if (creationPeriod > acceptableUiCalculationTime) { - UiUtils.UI_METRICS_LC_GROUP.w("Creation of %s took too much: %dms", getViewControllerClass(), creationPeriod); - } - } - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - tryCreateState(context); - } - - @Deprecated - @NonNull - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - return new PlaceholderView(inflater.getContext(), getViewControllerClass().getName()); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (view instanceof PlaceholderView) { - viewSubject.onNext(new NullablePair<>((PlaceholderView) view, savedInstanceState)); - } else { - Lc.assertion("View should be instanceof PlaceholderView"); - } - } - - @Override - public void onActivityCreated(@NonNull final View view, @NonNull final TActivity activity, @Nullable final Bundle savedInstanceState) { - super.onActivityCreated(view, activity, savedInstanceState); - activitySubject.onNext(new Optional<>(activity)); - } - - @Override - protected void onStart(@NonNull final View view, @NonNull final TActivity activity) { - super.onStart(view, activity); - started = true; - if (viewController != null) { - viewController.onStart(); - } - } - - @Override - protected void onAppear(@NonNull final View view, @NonNull final TActivity activity) { - super.onAppear(view, activity); - if (viewController != null) { - viewController.onAppear(); - } - } - - @Override - protected void onResume(@NonNull final View view, @NonNull final TActivity activity) { - super.onResume(view, activity); - if (viewController != null) { - viewController.onResume(); - } - } - - @Override - public void onLowMemory() { - super.onLowMemory(); - if (viewController != null) { - viewController.onLowMemory(); - } - } - - /** - * Calls when activity configuring ActionBar, Toolbar, Sidebar etc. - * If it will be called or not depends on {@link #hasOptionsMenu()} and {@link #isMenuVisible()}. - * - * @param menu The options menu in which you place your items; - * @param inflater Helper to inflate menu items. - */ - protected void onConfigureNavigation(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { - if (viewController != null) { - viewController.onConfigureNavigation(menu, inflater); - } - } - - @Override - public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - onConfigureNavigation(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(@NonNull final MenuItem item) { - return (viewController != null && viewController.onOptionsItemSelected(item)) || super.onOptionsItemSelected(item); - } - - private void onViewControllerChanged(@Nullable final ViewController viewController) { - if (this.viewController != null) { - this.viewController.onDestroy(); - } - this.viewController = viewController; - if (this.viewController != null) { - if (started) { - this.viewController.onStart(); - } - this.viewController.getActivity().reconfigureNavigation(); - } - } - - @Override - protected void onPause(@NonNull final View view, @NonNull final TActivity activity) { - super.onPause(view, activity); - if (viewController != null) { - viewController.onPause(); - } - } - - @Override - public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { - super.onSaveInstanceState(savedInstanceState); - if (viewController != null) { - viewController.onSaveInstanceState(savedInstanceState); - } - savedInstanceState.putSerializable(VIEW_CONTROLLER_STATE_EXTRA, state); - } - - @Override - protected void onDisappear(@NonNull final View view, @NonNull final TActivity activity) { - super.onDisappear(view, activity); - if (viewController != null) { - viewController.onDisappear(); - } - } - - @Override - protected void onStop(@NonNull final View view, @NonNull final TActivity activity) { - started = false; - if (viewController != null) { - viewController.onStop(); - } - super.onStop(view, activity); - } - - @Override - protected void onDestroyView(@NonNull final View view) { - viewSubject.onNext(new NullablePair<>(null, null)); - super.onDestroyView(view); - } - - @Override - public void onDetach() { - activitySubject.onNext(new Optional<>(null)); - super.onDetach(); - } - - @Override - public void onDestroy() { - viewControllerSubscription.unsubscribe(); - if (viewController != null && !viewController.isDestroyed()) { - viewController.onDestroy(); - viewController = null; - } - super.onDestroy(); - } - - @NonNull - @Override - public String toString() { - return super.toString() + "ViewController: " + getViewControllerClass(); - } - - private static class PlaceholderView extends FrameLayout { - - @NonNull - private final String tagName; - private long lastMeasureTime; - - public PlaceholderView(@NonNull final Context context, @NonNull final String tagName) { - super(context); - this.tagName = tagName; - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (inDebugMode && lastMeasureTime == 0) { - lastMeasureTime = SystemClock.uptimeMillis(); - } - } - - @Override - protected void onDraw(@NonNull final Canvas canvas) { - super.onDraw(canvas); - 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); - } - lastMeasureTime = 0; - } - } - - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java b/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java deleted file mode 100644 index 8613a49..0000000 --- a/src/main/java/ru/touchin/roboswag/components/navigation/fragments/ViewFragment.java +++ /dev/null @@ -1,263 +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.navigation.fragments; - -import android.os.Bundle; -import android.support.annotation.CallSuper; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.app.AppCompatActivity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import ru.touchin.roboswag.components.navigation.OnFragmentStartedListener; -import ru.touchin.roboswag.core.log.Lc; -import rx.functions.Action2; - -/** - * Created by Gavriil Sitnikov on 21/10/2015. - * Non-background fragment that have specific activity as a parent. - * - * @param Type of activity which to such fragment could be attached. - */ -public abstract class ViewFragment extends Fragment - implements OnFragmentStartedListener { - - private boolean appeared; - private boolean started; - - /** - * Returns if fragment have parent fragment. - * - * @return Returns true if fragment is in some fragment's children stack. - */ - public boolean isChildFragment() { - return getParentFragment() != null; - } - - /** - * Returns specific activity which to this fragment could be attached. - * - * @return Returns parent activity. - */ - @SuppressWarnings("unchecked") - @Nullable - protected final TActivity getBaseActivity() { - if (getActivity() == null) { - return null; - } - - try { - return (TActivity) getActivity(); - } catch (final ClassCastException exception) { - Lc.assertion(exception); - return null; - } - } - - @NonNull - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, - @Nullable final Bundle savedInstanceState) { - throw new IllegalStateException("Method onCreateView() should be overridden"); - } - - @Override - @CallSuper - public void onFragmentStarted(@NonNull final Fragment fragment) { - //do nothing - } - - @Deprecated - @Override - public void onActivityCreated(@Nullable final Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - if (getView() == null || getBaseActivity() == null) { - Lc.assertion("View and activity shouldn't be null"); - return; - } - onActivityCreated(getView(), getBaseActivity(), savedInstanceState); - } - - /** - * Replacement of {@link #onActivityCreated} with non null activity as first parameter. - * - * @param view Instantiated view. - * @param activity Activity which fragment attached to. - * @param savedInstanceState If the fragment is being re-created from a previous saved state, this is the state. - */ - @CallSuper - public void onActivityCreated(@NonNull final View view, @NonNull final TActivity activity, @Nullable final Bundle savedInstanceState) { - //do nothing - } - - private void callMethodAfterInstantiation(@NonNull final Action2 action) { - if (getView() == null || getBaseActivity() == null) { - Lc.assertion("View and activity shouldn't be null"); - return; - } - action.call(getView(), getBaseActivity()); - } - - @Deprecated - @Override - public void onStart() { - super.onStart(); - started = true; - callMethodAfterInstantiation(this::onStart); - } - - /** - * Replacement of {@link #onStart} with non null activity as first parameter. - * - * @param view Instantiated view. - * @param activity Activity which fragment attached to. - */ - @CallSuper - @SuppressWarnings("RestrictedApi") - //RestrictedApi: we need isMenuVisible() to check analytics rightly! - protected void onStart(@NonNull final View view, @NonNull final TActivity activity) { - if (getParentFragment() instanceof OnFragmentStartedListener) { - ((OnFragmentStartedListener) getParentFragment()).onFragmentStarted(this); - } else if (activity instanceof OnFragmentStartedListener) { - ((OnFragmentStartedListener) activity).onFragmentStarted(this); - } - if (!appeared && isMenuVisible()) { - onAppear(view, activity); - } - } - - /** - * Called when fragment is moved in started state and it's {@link #isMenuVisible()} sets to true. - * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. - * - * @param view Instantiated view. - * @param activity Activity which fragment attached to. - */ - protected void onAppear(@NonNull final View view, @NonNull final TActivity activity) { - appeared = true; - } - - @Deprecated - @Override - public void onResume() { - super.onResume(); - callMethodAfterInstantiation(this::onResume); - } - - /** - * Replacement of {@link #onResume} with non null activity as first parameter. - * - * @param view Instantiated view. - * @param activity Activity which fragment attached to. - */ - @CallSuper - protected void onResume(@NonNull final View view, @NonNull final TActivity activity) { - //do nothing - } - - @Override - public void setMenuVisibility(final boolean menuVisible) { - super.setMenuVisibility(menuVisible); - if (getBaseActivity() != null && getView() != null) { - if (!appeared && menuVisible && started) { - onAppear(getView(), getBaseActivity()); - } - if (appeared && (!menuVisible || !started)) { - onDisappear(getView(), getBaseActivity()); - } - } - } - - @Deprecated - @Override - public void onPause() { - callMethodAfterInstantiation(this::onPause); - super.onPause(); - } - - /** - * Replacement of {@link #onPause} with non null activity as first parameter. - * - * @param view Instantiated view. - * @param activity Activity which fragment attached to. - */ - @CallSuper - protected void onPause(@NonNull final View view, @NonNull final TActivity activity) { - // do nothing - } - - /** - * Called when fragment is moved in stopped state or it's {@link #isMenuVisible()} sets to false. - * Usually it is indicating that user can't see fragment on screen and useful to track analytics events. - * - * @param view Instantiated view. - * @param activity Activity which fragment attached to. - */ - protected void onDisappear(@NonNull final View view, @NonNull final TActivity activity) { - appeared = false; - } - - @Deprecated - @Override - public void onStop() { - started = false; - callMethodAfterInstantiation(this::onStop); - super.onStop(); - } - - /** - * Replacement of {@link #onStop} with non null activity as first parameter. - * - * @param view Instantiated view. - * @param activity Activity which fragment attached to. - */ - @CallSuper - protected void onStop(@NonNull final View view, @NonNull final TActivity activity) { - if (appeared) { - onDisappear(view, activity); - } - } - - @Deprecated - @Override - public void onDestroyView() { - if (getView() == null) { - Lc.assertion("View shouldn't be null"); - return; - } - onDestroyView(getView()); - super.onDestroyView(); - } - - /** - * Replacement of {@link #onDestroyView} with non null activity as first parameter. - * - * @param view Instantiated view. - */ - @CallSuper - protected void onDestroyView(@NonNull final View view) { - //do nothing - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/utils/BaseLifecycleBindable.java b/src/main/java/ru/touchin/roboswag/components/utils/BaseLifecycleBindable.java deleted file mode 100644 index 2271599..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/BaseLifecycleBindable.java +++ /dev/null @@ -1,287 +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; - -import android.support.annotation.NonNull; - -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; -import rx.Completable; -import rx.Observable; -import rx.Single; -import rx.Subscription; -import rx.android.schedulers.AndroidSchedulers; -import rx.exceptions.OnErrorThrowable; -import rx.functions.Action0; -import rx.functions.Action1; -import rx.functions.Actions; -import rx.subjects.BehaviorSubject; - -/** - * Created by Gavriil Sitnikov on 18/04/16. - * Simple implementation of {@link LifecycleBindable}. Could be used to not implement interface but use such object inside. - */ -@SuppressWarnings("PMD.TooManyMethods") -public class BaseLifecycleBindable implements LifecycleBindable { - - private static final String UNTIL_DESTROY_METHOD = "untilDestroy"; - private static final String UNTIL_STOP_METHOD = "untilStop"; - - @NonNull - private final BehaviorSubject isCreatedSubject = BehaviorSubject.create(); - @NonNull - private final BehaviorSubject isStartedSubject = BehaviorSubject.create(); - @NonNull - private final BehaviorSubject isInAfterSaving = BehaviorSubject.create(); - - /** - * Call it on parent's onCreate method. - */ - public void onCreate() { - isCreatedSubject.onNext(true); - } - - /** - * Call it on parent's onStart method. - */ - public void onStart() { - isStartedSubject.onNext(true); - } - - /** - * Call it on parent's onResume method. - * It is needed as sometimes onSaveInstanceState() calling after onPause() with no onStop call. So lifecycle object going in stopped state. - * In that case onResume will be called after onSaveInstanceState so lifecycle object is becoming started. - */ - public void onResume() { - isInAfterSaving.onNext(false); - } - - /** - * Call it on parent's onSaveInstanceState method. - */ - public void onSaveInstanceState() { - isInAfterSaving.onNext(true); - } - - /** - * Call it on parent's onStop method. - */ - public void onStop() { - isStartedSubject.onNext(false); - } - - /** - * Call it on parent's onDestroy method. - */ - public void onDestroy() { - isCreatedSubject.onNext(false); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilStop(observable, Actions.empty(), getActionThrowableForAssertion(codePoint, UNTIL_STOP_METHOD), Actions.empty()); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilStop(observable, onNextAction, getActionThrowableForAssertion(codePoint, UNTIL_STOP_METHOD), Actions.empty()); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return untilStop(observable, onNextAction, onErrorAction, Actions.empty()); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return until(observable.delay(item -> isInAfterSaving.first(inAfterSaving -> !inAfterSaving)), - isStartedSubject.map(started -> !started), - onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilStop(single, Actions.empty(), getActionThrowableForAssertion(codePoint, UNTIL_STOP_METHOD)); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilStop(single, onSuccessAction, getActionThrowableForAssertion(codePoint, UNTIL_STOP_METHOD)); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return untilStop(single.toObservable(), onSuccessAction, onErrorAction, Actions.empty()); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilStop(completable, Actions.empty(), getActionThrowableForAssertion(codePoint, UNTIL_STOP_METHOD)); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilStop(completable, onCompletedAction, getActionThrowableForAssertion(codePoint, UNTIL_STOP_METHOD)); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return untilStop(completable.toObservable(), Actions.empty(), onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilDestroy(observable, Actions.empty(), getActionThrowableForAssertion(codePoint, UNTIL_DESTROY_METHOD), Actions.empty()); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilDestroy(observable, onNextAction, getActionThrowableForAssertion(codePoint, UNTIL_DESTROY_METHOD), Actions.empty()); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return untilDestroy(observable, onNextAction, onErrorAction, Actions.empty()); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return until(observable, isCreatedSubject.map(created -> !created), onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilDestroy(single, Actions.empty(), getActionThrowableForAssertion(codePoint, UNTIL_DESTROY_METHOD)); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilDestroy(single, onSuccessAction, getActionThrowableForAssertion(codePoint, UNTIL_DESTROY_METHOD)); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return until(single.toObservable(), isCreatedSubject.map(created -> !created), onSuccessAction, onErrorAction, Actions.empty()); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilDestroy(completable, Actions.empty(), getActionThrowableForAssertion(codePoint, UNTIL_DESTROY_METHOD)); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - final String codePoint = Lc.getCodePoint(this, 2); - return untilDestroy(completable, onCompletedAction, getActionThrowableForAssertion(codePoint, UNTIL_DESTROY_METHOD)); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return until(completable.toObservable(), isCreatedSubject.map(created -> !created), Actions.empty(), onErrorAction, onCompletedAction); - } - - @NonNull - private Subscription until(@NonNull final Observable observable, - @NonNull final Observable conditionSubject, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - final Observable actualObservable; - if (onNextAction == Actions.empty() && onErrorAction == (Action1) Actions.empty() && onCompletedAction == Actions.empty()) { - actualObservable = observable; - } else { - actualObservable = observable.observeOn(AndroidSchedulers.mainThread()) - .doOnCompleted(onCompletedAction) - .doOnNext(onNextAction) - .doOnError(onErrorAction); - } - - return isCreatedSubject.first() - .switchMap(created -> created ? actualObservable : Observable.empty()) - .takeUntil(conditionSubject.filter(condition -> condition)) - .onErrorResumeNext(throwable -> { - final boolean isRxError = throwable instanceof OnErrorThrowable; - if ((!isRxError && throwable instanceof RuntimeException) - || (isRxError && throwable.getCause() instanceof RuntimeException)) { - Lc.assertion(throwable); - } - return Observable.empty(); - }) - .subscribe(); - } - - @NonNull - private Action1 getActionThrowableForAssertion(@NonNull final String codePoint, @NonNull final String method) { - return throwable -> Lc.assertion(new ShouldNotHappenException("Unexpected error on " + method + " at " + codePoint, throwable)); - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/utils/LifecycleBindable.java b/src/main/java/ru/touchin/roboswag/components/utils/LifecycleBindable.java deleted file mode 100644 index f157356..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/LifecycleBindable.java +++ /dev/null @@ -1,314 +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; - -import android.support.annotation.NonNull; - -import rx.Completable; -import rx.CompletableSubscriber; -import rx.Observable; -import rx.Single; -import rx.SingleSubscriber; -import rx.Subscriber; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; - -/** - * Created by Gavriil Sitnikov on 15/04/16. - * Interface that should be implemented by lifecycle-based elements ({@link android.app.Activity}, {@link android.support.v4.app.Fragment} etc.) - * to not manually manage subscriptions. - * Use {@link #untilStop(Observable)} method to subscribe to observable where you want and unsubscribe onStop. - * Use {@link #untilDestroy(Observable)} method to subscribe to observable where you want and unsubscribe onDestroy. - */ -@SuppressWarnings("PMD.TooManyMethods") -public interface LifecycleBindable { - - /** - * Method should be used to guarantee that observable won't be subscribed after onStop. - * It is automatically subscribing to the observable. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if observable can emit them. - * - * @param observable {@link Observable} to subscribe until onStop; - * @param Type of emitted by observable items; - * @return {@link Subscription} which will unsubscribes from observable onStop. - */ - @NonNull - Subscription untilStop(@NonNull Observable observable); - - /** - * Method should be used to guarantee that observable won't be subscribed after onStop. - * It is automatically subscribing to the observable and calls onNextAction on every emitted item. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if observable can emit them. - * - * @param observable {@link Observable} to subscribe until onStop; - * @param onNextAction Action which will raise on every {@link Subscriber#onNext(Object)} item; - * @param Type of emitted by observable items; - * @return {@link Subscription} which will unsubscribes from observable onStop. - */ - @NonNull - Subscription untilStop(@NonNull Observable observable, @NonNull Action1 onNextAction); - - /** - * Method should be used to guarantee that observable won't be subscribed after onStop. - * It is automatically subscribing to the observable and calls onNextAction and onErrorAction on observable events. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if observable can emit them. - * - * @param observable {@link Observable} to subscribe until onStop; - * @param onNextAction Action which will raise on every {@link Subscriber#onNext(Object)} item; - * @param onErrorAction Action which will raise on every {@link Subscriber#onError(Throwable)} throwable; - * @param Type of emitted by observable items; - * @return {@link Subscription} which will unsubscribes from observable onStop. - */ - @NonNull - Subscription untilStop(@NonNull Observable observable, @NonNull Action1 onNextAction, @NonNull Action1 onErrorAction); - - /** - * Method should be used to guarantee that observable won't be subscribed after onStop. - * It is automatically subscribing to the observable and calls onNextAction, onErrorAction and onCompletedAction on observable events. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if observable can emit them. - * - * @param observable {@link Observable} to subscribe until onStop; - * @param onNextAction Action which will raise on every {@link Subscriber#onNext(Object)} item; - * @param onErrorAction Action which will raise on every {@link Subscriber#onError(Throwable)} throwable; - * @param onCompletedAction Action which will raise at {@link Subscriber#onCompleted()} on completion of observable; - * @param Type of emitted by observable items; - * @return {@link Subscription} which is wrapping source observable to unsubscribe from it onStop. - */ - @NonNull - Subscription untilStop(@NonNull Observable observable, - @NonNull Action1 onNextAction, @NonNull Action1 onErrorAction, @NonNull Action0 onCompletedAction); - - /** - * Method should be used to guarantee that single won't be subscribed after onStop. - * It is automatically subscribing to the single. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if single can emit them. - * - * @param single {@link Single} to subscribe until onStop; - * @param Type of emitted by single item; - * @return {@link Subscription} which will unsubscribes from single onStop. - */ - @NonNull - Subscription untilStop(@NonNull Single single); - - /** - * Method should be used to guarantee that single won't be subscribed after onStop. - * It is automatically subscribing to the single and calls onSuccessAction on the emitted item. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if single can emit them. - * - * @param single {@link Single} to subscribe until onStop; - * @param onSuccessAction Action which will raise on every {@link SingleSubscriber#onSuccess(Object)} item; - * @param Type of emitted by single item; - * @return {@link Subscription} which will unsubscribes from single onStop. - */ - @NonNull - Subscription untilStop(@NonNull Single single, @NonNull Action1 onSuccessAction); - - /** - * Method should be used to guarantee that single won't be subscribed after onStop. - * It is automatically subscribing to the single and calls onSuccessAction and onErrorAction on single events. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if single can emit them. - * - * @param single {@link Single} to subscribe until onStop; - * @param onSuccessAction Action which will raise on every {@link SingleSubscriber#onSuccess(Object)} item; - * @param onErrorAction Action which will raise on every {@link SingleSubscriber#onError(Throwable)} throwable; - * @param Type of emitted by observable items; - * @return {@link Subscription} which is wrapping source single to unsubscribe from it onStop. - */ - @NonNull - Subscription untilStop(@NonNull Single single, @NonNull Action1 onSuccessAction, @NonNull Action1 onErrorAction); - - /** - * Method should be used to guarantee that completable won't be subscribed after onStop. - * It is automatically subscribing to the completable. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if completable can emit them. - * - * @param completable {@link Completable} to subscribe until onStop; - * @return {@link Subscription} which will unsubscribes from completable onStop. - */ - @NonNull - Subscription untilStop(@NonNull Completable completable); - - /** - * Method should be used to guarantee that completable won't be subscribed after onStop. - * It is automatically subscribing to the completable and calls onCompletedAction on completable item. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if completable can emit them. - * - * @param completable {@link Completable} to subscribe until onStop; - * @param onCompletedAction Action which will raise at {@link CompletableSubscriber#onCompleted()} on completion of observable; - * @return {@link Subscription} which is wrapping source completable to unsubscribe from it onStop. - */ - @NonNull - Subscription untilStop(@NonNull Completable completable, @NonNull Action0 onCompletedAction); - - /** - * Method should be used to guarantee that completable won't be subscribed after onStop. - * It is automatically subscribing to the completable and calls onCompletedAction and onErrorAction on completable item. - * Usually it is using to stop requests/execution while element is off or to not do illegal actions after onStop like fragment's stack changing. - * Don't forget to process errors if completable can emit them. - * - * @param completable {@link Completable} to subscribe until onStop; - * @param onCompletedAction Action which will raise at {@link CompletableSubscriber#onCompleted()} on completion of observable; - * @param onErrorAction Action which will raise on every {@link CompletableSubscriber#onError(Throwable)} throwable; - * @return {@link Subscription} which is wrapping source completable to unsubscribe from it onStop. - */ - @NonNull - Subscription untilStop(@NonNull Completable completable, @NonNull Action0 onCompletedAction, @NonNull Action1 onErrorAction); - - /** - * Method should be used to guarantee that observable won't be subscribed after onDestroy. - * It is automatically subscribing to the observable. - * Don't forget to process errors if observable can emit them. - * - * @param observable {@link Observable} to subscribe until onDestroy; - * @param Type of emitted by observable items; - * @return {@link Subscription} which is wrapping source observable to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Observable observable); - - /** - * Method should be used to guarantee that observable won't be subscribed after onDestroy. - * It is automatically subscribing to the observable and calls onNextAction on every emitted item. - * Don't forget to process errors if observable can emit them. - * - * @param observable {@link Observable} to subscribe until onDestroy; - * @param onNextAction Action which will raise on every {@link Subscriber#onNext(Object)} item; - * @param Type of emitted by observable items; - * @return {@link Subscription} which is wrapping source observable to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Observable observable, @NonNull Action1 onNextAction); - - /** - * Method should be used to guarantee that observable won't be subscribed after onDestroy. - * It is automatically subscribing to the observable and calls onNextAction and onErrorAction on observable events. - * Don't forget to process errors if observable can emit them. - * - * @param observable {@link Observable} to subscribe until onDestroy; - * @param onNextAction Action which will raise on every {@link Subscriber#onNext(Object)} item; - * @param onErrorAction Action which will raise on every {@link Subscriber#onError(Throwable)} throwable; - * @param Type of emitted by observable items; - * @return {@link Subscription} which is wrapping source observable to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Observable observable, @NonNull Action1 onNextAction, @NonNull Action1 onErrorAction); - - /** - * Method should be used to guarantee that observable won't be subscribed after onDestroy. - * It is automatically subscribing to the observable and calls onNextAction, onErrorAction and onCompletedAction on observable events. - * Don't forget to process errors if observable can emit them. - * - * @param observable {@link Observable} to subscribe until onDestroy; - * @param onNextAction Action which will raise on every {@link Subscriber#onNext(Object)} item; - * @param onErrorAction Action which will raise on every {@link Subscriber#onError(Throwable)} throwable; - * @param onCompletedAction Action which will raise at {@link Subscriber#onCompleted()} on completion of observable; - * @param Type of emitted by observable items; - * @return {@link Subscription} which is wrapping source observable to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Observable observable, - @NonNull Action1 onNextAction, @NonNull Action1 onErrorAction, @NonNull Action0 onCompletedAction); - - /** - * Method should be used to guarantee that single won't be subscribed after onDestroy. - * It is automatically subscribing to the single. - * Don't forget to process errors if single can emit them. - * - * @param single {@link Single} to subscribe until onDestroy; - * @param Type of emitted by single items; - * @return {@link Subscription} which is wrapping source single to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Single single); - - /** - * Method should be used to guarantee that single won't be subscribed after onDestroy. - * It is automatically subscribing to the single and calls onSuccessAction on every emitted item. - * Don't forget to process errors if single can emit them. - * - * @param single {@link Single} to subscribe until onDestroy; - * @param onSuccessAction Action which will raise on every {@link SingleSubscriber#onSuccess(Object)} item; - * @param Type of emitted by single items; - * @return {@link Subscription} which is wrapping source single to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Single single, @NonNull Action1 onSuccessAction); - - /** - * Method should be used to guarantee that single won't be subscribed after onDestroy. - * It is automatically subscribing to the single and calls onSuccessAction and onErrorAction on single events. - * Don't forget to process errors if single can emit them. - * - * @param single {@link Single} to subscribe until onDestroy; - * @param onSuccessAction Action which will raise on every {@link SingleSubscriber#onSuccess(Object)} item; - * @param onErrorAction Action which will raise on every {@link SingleSubscriber#onError(Throwable)} throwable; - * @param Type of emitted by single items; - * @return {@link Subscription} which is wrapping source single to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Single single, @NonNull Action1 onSuccessAction, @NonNull Action1 onErrorAction); - - /** - * Method should be used to guarantee that completable won't be subscribed after onDestroy. - * It is automatically subscribing to the completable. - * Don't forget to process errors if completable can emit them. - * - * @param completable {@link Completable} to subscribe until onDestroy; - * @return {@link Subscription} which is wrapping source completable to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Completable completable); - - /** - * Method should be used to guarantee that completable won't be subscribed after onDestroy. - * It is automatically subscribing to the completable and calls onCompletedAction on completable item. - * Don't forget to process errors if single can emit them. - * - * @param completable {@link Completable} to subscribe until onDestroy; - * @param onCompletedAction Action which will raise on every {@link CompletableSubscriber#onCompleted()} item; - * @return {@link Subscription} which is wrapping source single to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Completable completable, @NonNull Action0 onCompletedAction); - - /** - * Method should be used to guarantee that completable won't be subscribed after onDestroy. - * It is automatically subscribing to the completable and calls onCompletedAction and onErrorAction on completable events. - * Don't forget to process errors if completable can emit them. - * - * @param completable {@link Completable} to subscribe until onDestroy; - * @param onCompletedAction Action which will raise on every {@link CompletableSubscriber#onCompleted()} item; - * @param onErrorAction Action which will raise on every {@link CompletableSubscriber#onError(Throwable)} throwable; - * @return {@link Subscription} which is wrapping source completable to unsubscribe from it onDestroy. - */ - @NonNull - Subscription untilDestroy(@NonNull Completable completable, @NonNull Action0 onCompletedAction, @NonNull Action1 onErrorAction); - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/utils/Logic.java b/src/main/java/ru/touchin/roboswag/components/utils/Logic.java deleted file mode 100644 index cdce5c0..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/Logic.java +++ /dev/null @@ -1,110 +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; - -import android.content.Context; -import android.support.annotation.NonNull; - -import java.lang.ref.WeakReference; -import java.lang.reflect.Constructor; -import java.util.HashMap; -import java.util.Map; - -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; -import rx.Observable; - -/** - * Created by Gavriil Sitnikov on 24/03/16. - * Base class representing application's logic. - * In specific application it should be child of it (usually one) which contains all methods/objects related to logic. - * It should contains interface to work with API/preferences/database/file system/system parameters etc. - * Be sure that all objects/instances/services created to represents logic are not getting a lot of time to be instantiated, if they take a lot time - * for instantiation then it is wrong logic and it should be moved into asynchronous operations via {@link Observable} or so. - * Also it shouldn't create massive data objects and a lot of objects instantly. Basically it should just create bunch of interfaces inside - * which will allows to access to some logic methods. - * In fact it is similar to dependency injection pattern but with full control of instantiation and only one single instance of {@link Logic} per app. - * If you want to use it then just create getter in {@link android.app.Service}/{@link android.app.Activity}/{@link android.content.BroadcastReceiver} - * or any else context-based elements and do not forget to store reference to {@link Logic} into field because else it will be consumed by GC. - * Sample of {@link Logic} using is in {@link ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity}. - * NOTE: Ideally creation of logic should be asynchronous and stored in specific {@link android.app.Service} so it should be accessed - * asynchronously via {@link Observable} or so. But in fact it requires {@link android.app.Service} plus more complex methods to access to logic. - * So currently it is more simple to access via simple bridge based on singletons stored into {@link WeakReference} because anyway instantiation of - * logic have to be as fast as it can. If it's not then it is just a bug and problem of optimization. - */ -public class Logic { - - private static final Map, WeakReference> LOGIC_INSTANCES = new HashMap<>(); - - /** - * Returns instance of {@link Logic} depends on class. There should be no more than one instance per class. - * - * @param context Context of application where this {@link Logic} related to; - * @param logicClass Class of {@link Logic}; - * @param Type of class of {@link Logic}; - * @return Instance of {@link Logic}. - */ - @SuppressWarnings({"unchecked", "PMD.SingletonClassReturningNewInstance"}) - //SingletonClassReturningNewInstance: it is OK to create instance every time if WeakReference have died - @NonNull - public static T getInstance(@NonNull final Context context, @NonNull final Class logicClass) { - T result; - synchronized (LOGIC_INSTANCES) { - final WeakReference reference = LOGIC_INSTANCES.get(logicClass); - result = reference != null ? (T) reference.get() : null; - if (result == null) { - result = constructLogic(context.getApplicationContext(), logicClass); - LOGIC_INSTANCES.put(logicClass, new WeakReference<>(result)); - } - } - return result; - } - - @NonNull - @SuppressWarnings("unchecked") - private static T constructLogic(@NonNull final Context context, @NonNull final Class logicClass) { - if (logicClass.getConstructors().length != 1 || logicClass.getConstructors()[0].getParameterTypes().length != 1) { - throw new ShouldNotHappenException("There should be only one public constructor(Context) for class " + logicClass); - } - final Constructor constructor = logicClass.getConstructors()[0]; - try { - return (T) constructor.newInstance(context); - } catch (final Exception exception) { - throw new ShouldNotHappenException(exception); - } - } - - @NonNull - private final Context context; - - public Logic(@NonNull final Context context) { - this.context = context; - } - - /** - * Returns {@link android.app.Application}'s context. - * - * @return Context (possibly application). - */ - @NonNull - public Context getContext() { - return context; - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/utils/Typefaces.java b/src/main/java/ru/touchin/roboswag/components/utils/Typefaces.java deleted file mode 100644 index d8be010..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/Typefaces.java +++ /dev/null @@ -1,105 +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; - -import android.content.Context; -import android.content.res.AssetManager; -import android.content.res.TypedArray; -import android.graphics.Typeface; -import android.support.annotation.NonNull; -import android.support.annotation.StyleableRes; -import android.util.AttributeSet; - -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; - -/** - * Created by Gavriil Sitnikov on 18/07/2014. - * Manager for typefaces stored in 'assets/fonts' folder. - */ -public final class Typefaces { - - private static final Map TYPEFACES_MAP = new HashMap<>(); - - /** - * Returns {@link Typeface} by name from assets 'fonts' folder. - * - * @param context Context of assets where typeface file stored in; - * @param name Full name of typeface (without extension, e.g. 'Roboto-Regular'); - * @return {@link Typeface} from assets. - */ - @NonNull - public static Typeface getByName(@NonNull final Context context, @NonNull final String name) { - synchronized (TYPEFACES_MAP) { - Typeface result = TYPEFACES_MAP.get(name); - if (result == null) { - final AssetManager assetManager = context.getAssets(); - result = Typeface.DEFAULT; - try { - final List fonts = Arrays.asList(assetManager.list("fonts")); - if (fonts.contains(name + ".ttf")) { - result = Typeface.createFromAsset(assetManager, "fonts/" + name + ".ttf"); - } else if (fonts.contains(name + ".otf")) { - result = Typeface.createFromAsset(assetManager, "fonts/" + name + ".otf"); - } else { - Lc.assertion("Can't find .otf or .ttf file in assets folder 'fonts' with name: " + name); - } - } catch (final IOException exception) { - Lc.assertion(new ShouldNotHappenException("Can't get font " + name + '.' - + "Did you forget to create assets folder named 'fonts'?", exception)); - } - TYPEFACES_MAP.put(name, result); - } - return result; - } - } - - /** - * Returns {@link Typeface} by name from assets 'fonts' folder. - * - * @param context Context of assets where typeface file stored in; - * @param attrs Attributes of view to get font from; - * @param styleableId Id of attribute set; - * @param attributeId Id of attribute; - * @return {@link Typeface} from assets. - */ - @NonNull - public static Typeface getFromAttributes(@NonNull final Context context, @NonNull final AttributeSet attrs, - @NonNull @StyleableRes final int[] styleableId, @StyleableRes final int attributeId) { - final TypedArray typedArray = context.obtainStyledAttributes(attrs, styleableId); - final String customTypeface = typedArray.getString(attributeId); - typedArray.recycle(); - if (customTypeface != null) { - return getByName(context, customTypeface); - } - Lc.w("Couldn't find custom typeface. Returns default"); - return Typeface.DEFAULT; - } - - private Typefaces() { - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/utils/audio/HeadsetStateObserver.java b/src/main/java/ru/touchin/roboswag/components/utils/audio/HeadsetStateObserver.java deleted file mode 100644 index eea069f..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/audio/HeadsetStateObserver.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package ru.touchin.roboswag.components.utils.audio; - -import android.bluetooth.BluetoothA2dp; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.media.AudioManager; -import android.support.annotation.NonNull; - -import rx.Observable; -import rx.subjects.BehaviorSubject; - -/** - * Created by Gavriil Sitnikov on 02/11/2015. - * Simple observer of wired or wireless (bluetooth A2DP) headsets state (connected or not). - *
You require android.permission.BLUETOOTH and API level >= 11 if want to observe wireless headset state - */ -public final class HeadsetStateObserver { - - @NonNull - private final AudioManager audioManager; - @NonNull - private final Observable connectedObservable; - - public HeadsetStateObserver(@NonNull final Context context) { - audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - connectedObservable = Observable - .fromCallable(() -> new IsConnectedReceiver(audioManager)) - .switchMap(isConnectedReceiver -> Observable.combineLatest(isConnectedReceiver.isWiredConnectedChangedEvent, - isConnectedReceiver.isWirelessConnectedChangedEvent, - (isWiredConnected, isWirelessConnected) -> isWiredConnected || isWirelessConnected) - .distinctUntilChanged() - .doOnSubscribe(() -> { - final IntentFilter headsetStateIntentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); - headsetStateIntentFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); - context.registerReceiver(isConnectedReceiver, headsetStateIntentFilter); - }) - .doOnUnsubscribe(() -> context.unregisterReceiver(isConnectedReceiver))) - .replay(1) - .refCount(); - } - - /** - * Returns if wired or wireless headset is connected. - * - * @return True if headset is connected. - */ - @SuppressWarnings("deprecation") - public boolean isConnected() { - return audioManager.isWiredHeadsetOn() || audioManager.isBluetoothA2dpOn(); - } - - /** - * Observes connection state of headset. - * - * @return Returns observable which will provide current connection state and any of it's udpdate. - */ - @NonNull - public Observable observeIsConnected() { - return connectedObservable; - } - - private static class IsConnectedReceiver extends BroadcastReceiver { - - @NonNull - private final BehaviorSubject isWiredConnectedChangedEvent; - @NonNull - private final BehaviorSubject isWirelessConnectedChangedEvent; - - @SuppressWarnings("deprecation") - public IsConnectedReceiver(@NonNull final AudioManager audioManager) { - super(); - isWiredConnectedChangedEvent = BehaviorSubject.create(audioManager.isWiredHeadsetOn()); - isWirelessConnectedChangedEvent = BehaviorSubject.create(audioManager.isBluetoothA2dpOn()); - } - - @Override - public void onReceive(@NonNull final Context context, @NonNull final Intent intent) { - if (Intent.ACTION_HEADSET_PLUG.equals(intent.getAction()) && !isInitialStickyBroadcast()) { - isWiredConnectedChangedEvent.onNext(intent.getIntExtra("state", 0) != 0); - } - if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { - final int bluetoothState = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTED); - switch (bluetoothState) { - case BluetoothA2dp.STATE_DISCONNECTED: - isWirelessConnectedChangedEvent.onNext(false); - break; - case BluetoothA2dp.STATE_CONNECTED: - isWirelessConnectedChangedEvent.onNext(true); - break; - default: - break; - } - } - } - - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/utils/audio/VolumeController.java b/src/main/java/ru/touchin/roboswag/components/utils/audio/VolumeController.java deleted file mode 100644 index 5a70c4d..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/audio/VolumeController.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) - * - * This file is part of RoboSwag library. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package ru.touchin.roboswag.components.utils.audio; - -import android.content.Context; -import android.database.ContentObserver; -import android.media.AudioManager; -import android.os.Handler; -import android.os.Looper; -import android.provider.Settings; -import android.support.annotation.NonNull; - -import ru.touchin.roboswag.core.log.Lc; -import ru.touchin.roboswag.core.utils.ShouldNotHappenException; -import rx.Observable; -import rx.subjects.PublishSubject; - -/** - * Created by Gavriil Sitnikov on 02/11/2015. - * Simple class to control and observe volume of specific stream type (phone call, music etc.). - */ -public final class VolumeController { - - @NonNull - private final AudioManager audioManager; - private final int maxVolume; - @NonNull - private final Observable volumeObservable; - @NonNull - private final PublishSubject selfVolumeChangedEvent = PublishSubject.create(); - - public VolumeController(@NonNull final Context context) { - this(context, AudioManager.STREAM_MUSIC); - } - - public VolumeController(@NonNull final Context context, final int streamType) { - audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - maxVolume = audioManager.getStreamMaxVolume(streamType); - volumeObservable = Observable - .fromCallable(VolumeObserver::new) - .switchMap(volumeObserver -> selfVolumeChangedEvent - .mergeWith(volumeObserver.systemVolumeChangedEvent - .map(event -> getVolume()) - .doOnSubscribe(() -> context.getContentResolver() - .registerContentObserver(Settings.System.CONTENT_URI, true, volumeObserver)) - .doOnUnsubscribe(() -> context.getContentResolver() - .unregisterContentObserver(volumeObserver))) - .startWith(getVolume())) - .distinctUntilChanged() - .replay(1) - .refCount(); - } - - /** - * Max volume amount to set. - * - * @return max volume. - */ - public int getMaxVolume() { - return maxVolume; - } - - /** - * Sets volume. - * - * @param volume Volume value to set from 0 to {@link #getMaxVolume()}. - */ - public void setVolume(final int volume) { - if (volume < 0 || volume > maxVolume) { - Lc.assertion(new ShouldNotHappenException("Volume: " + volume + " out of bounds [0," + maxVolume + ']')); - return; - } - if (getVolume() != volume) { - audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); - selfVolumeChangedEvent.onNext(volume); - } - } - - /** - * Returns volume. - * - * @return Returns volume value from 0 to {@link #getMaxVolume()}. - */ - public int getVolume() { - return audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); - } - - /** - * Observes current volume. - * - * @return Observable which will provide current volume and then it's updates. - */ - @NonNull - public Observable observeVolume() { - return volumeObservable; - } - - private static class VolumeObserver extends ContentObserver { - - @NonNull - private final PublishSubject systemVolumeChangedEvent = PublishSubject.create(); - - public VolumeObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public void onChange(final boolean selfChange) { - super.onChange(selfChange); - systemVolumeChangedEvent.onNext(null); - } - - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/utils/images/BlurUtils.java b/src/main/java/ru/touchin/roboswag/components/utils/images/BlurUtils.java deleted file mode 100644 index 6f3837d..0000000 --- a/src/main/java/ru/touchin/roboswag/components/utils/images/BlurUtils.java +++ /dev/null @@ -1,323 +0,0 @@ -/** - * Copyright (C) 2015 Wasabeef - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ru.touchin.roboswag.components.utils.images; - -import android.annotation.TargetApi; -import android.content.Context; -import android.graphics.Bitmap; -import android.os.Build; -import android.renderscript.Allocation; -import android.renderscript.Element; -import android.renderscript.RSRuntimeException; -import android.renderscript.RenderScript; -import android.renderscript.ScriptIntrinsicBlur; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -public final class BlurUtils { - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @Nullable - public static Bitmap blurRenderscript(@NonNull final Context context, @NonNull final Bitmap bitmap, final int radius) throws RSRuntimeException { - RenderScript rs = null; - Allocation input = null; - Allocation output = null; - ScriptIntrinsicBlur blur = null; - try { - rs = RenderScript.create(context); - rs.setMessageHandler(new RenderScript.RSMessageHandler()); - input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE, - Allocation.USAGE_SCRIPT); - output = Allocation.createTyped(rs, input.getType()); - blur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); - - blur.setInput(input); - blur.setRadius(radius); - blur.forEach(output); - output.copyTo(bitmap); - } finally { - if (rs != null) { - rs.destroy(); - } - if (input != null) { - input.destroy(); - } - if (output != null) { - output.destroy(); - } - if (blur != null) { - blur.destroy(); - } - } - - return bitmap; - } - - @Nullable - @SuppressWarnings({"PMD.ExcessiveMethodLength", "PMD.CyclomaticComplexity", "PMD.ModifiedCyclomaticComplexity", "PMD.StdCyclomaticComplexity", - "PMD.NcssMethodCount", "PMD.NPathComplexity", "checkstyle:MethodLength", "checkstyle:LocalFinalVariableName", - "checkstyle:ArrayTypeStyle", "checkstyle:InnerAssignment", "checkstyle:LocalVariableName"}) - public static Bitmap blurFast(@NonNull final Bitmap sentBitmap, final int radius, final boolean canReuseInBitmap) { - - // Stack Blur v1.0 from - // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html - // - // Java Author: Mario Klingemann - // http://incubator.quasimondo.com - // created Feburary 29, 2004 - // Android port : Yahel Bouaziz - // http://www.kayenko.com - // ported april 5th, 2012 - - // This is a compromise between Gaussian Blur and Box blur - // It creates much better looking blurs than Box Blur, but is - // 7x faster than my Gaussian Blur implementation. - // - // I called it Stack Blur because this describes best how this - // filter works internally: it creates a kind of moving stack - // of colors whilst scanning through the image. Thereby it - // just has to add one new block of color to the right side - // of the stack and remove the leftmost color. The remaining - // colors on the topmost layer of the stack are either added on - // or reduced by one, depending on if they are on the right or - // on the left side of the stack. - // - // If you are using this algorithm in your code please add - // the following line: - // - // Stack Blur Algorithm by Mario Klingemann - - final Bitmap bitmap; - if (canReuseInBitmap) { - bitmap = sentBitmap; - } else { - bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); - } - - if (radius < 1) { - return null; - } - - final int w = bitmap.getWidth(); - final int h = bitmap.getHeight(); - - final int[] pix = new int[w * h]; - bitmap.getPixels(pix, 0, w, 0, 0, w, h); - - final int wm = w - 1; - final int hm = h - 1; - final int wh = w * h; - final int div = radius + radius + 1; - - final int r[] = new int[wh]; - final int g[] = new int[wh]; - final int b[] = new int[wh]; - int rsum; - int gsum; - int bsum; - int x; - int i; - int p; - int yp; - int yi; - int yw; - final int vmin[] = new int[Math.max(w, h)]; - - int divsum = (div + 1) >> 1; - divsum *= divsum; - final int dv[] = new int[256 * divsum]; - for (i = 0; i < 256 * divsum; i++) { - dv[i] = i / divsum; - } - - yw = yi = 0; - - final int[][] stack = new int[div][3]; - int stackpointer; - int stackstart; - int[] sir; - int rbs; - final int r1 = radius + 1; - int routsum; - int goutsum; - int boutsum; - int rinsum; - int ginsum; - int binsum; - - int y; - - for (y = 0; y < h; y++) { - rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; - for (i = -radius; i <= radius; i++) { - p = pix[yi + Math.min(wm, Math.max(i, 0))]; - sir = stack[i + radius]; - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = p & 0x0000ff; - rbs = r1 - Math.abs(i); - rsum += sir[0] * rbs; - gsum += sir[1] * rbs; - bsum += sir[2] * rbs; - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - } - } - stackpointer = radius; - - for (x = 0; x < w; x++) { - - r[yi] = dv[rsum]; - g[yi] = dv[gsum]; - b[yi] = dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - - if (y == 0) { - vmin[x] = Math.min(x + radius + 1, wm); - } - p = pix[yw + vmin[x]]; - - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = p & 0x0000ff; - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[stackpointer % div]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - - yi++; - } - yw += w; - } - for (x = 0; x < w; x++) { - rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; - yp = -radius * w; - for (i = -radius; i <= radius; i++) { - yi = Math.max(0, yp) + x; - - sir = stack[i + radius]; - - sir[0] = r[yi]; - sir[1] = g[yi]; - sir[2] = b[yi]; - - rbs = r1 - Math.abs(i); - - rsum += r[yi] * rbs; - gsum += g[yi] * rbs; - bsum += b[yi] * rbs; - - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - } - - if (i < hm) { - yp += w; - } - } - yi = x; - stackpointer = radius; - for (y = 0; y < h; y++) { - // Preserve alpha channel: ( 0xff000000 & pix[yi] ) - pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - - if (x == 0) { - vmin[y] = Math.min(y + r1, hm) * w; - } - p = x + vmin[y]; - - sir[0] = r[p]; - sir[1] = g[p]; - sir[2] = b[p]; - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[stackpointer]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - - yi += w; - } - } - - bitmap.setPixels(pix, 0, w, 0, 0, w, h); - - return bitmap; - } - - private BlurUtils() { - } - -} diff --git a/src/main/java/ru/touchin/roboswag/components/views/AspectRatioFrameLayout.java b/src/main/java/ru/touchin/roboswag/components/views/AspectRatioFrameLayout.java deleted file mode 100644 index 3291bbe..0000000 --- a/src/main/java/ru/touchin/roboswag/components/views/AspectRatioFrameLayout.java +++ /dev/null @@ -1,254 +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.graphics.Point; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import ru.touchin.roboswag.components.R; - - -/** - * Created by Gavriil Sitnikov on 01/07/14. - * FrameLayout that holds specific aspect ratio sizes. - * For example if aspect ratio equals 1.0 then this view will layout as square. - */ -public class AspectRatioFrameLayout extends FrameLayout { - - private static final float DEFAULT_ASPECT_RATIO = 1.0f; - private static final float EPSILON = 0.0000001f; - - private float aspectRatio; - private boolean wrapToContent; - - public AspectRatioFrameLayout(@NonNull final Context context) { - this(context, null); - } - - public AspectRatioFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) { - this(context, attrs, 0); - } - - public AspectRatioFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyle) { - super(context, attrs, defStyle); - - if (attrs == null) { - wrapToContent = false; - aspectRatio = DEFAULT_ASPECT_RATIO; - } else { - final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AspectRatioFrameLayout); - wrapToContent = typedArray.getBoolean(R.styleable.AspectRatioFrameLayout_wrapToContent, false); - aspectRatio = typedArray.getFloat(R.styleable.AspectRatioFrameLayout_aspectRatio, DEFAULT_ASPECT_RATIO); - typedArray.recycle(); - } - } - - /* Returns aspect ratio of layout */ - public float getAspectRatio() { - return aspectRatio; - } - - /* Sets aspect ratio of layout */ - public void setAspectRatio(final float aspectRatio) { - if (Math.abs(aspectRatio - this.aspectRatio) < EPSILON) { - return; - } - - this.aspectRatio = aspectRatio; - requestLayout(); - } - - /* Returns if layout is wrapping to content but holds aspect ratio */ - - /** - * Returns if layout is wrapping to content but holds aspect ratio. - * If it is true it means that minimum size of view will equals to maximum size of it's child (biggest width or height) depends on aspect ratio. - * Else maximum size of view will equals to minimum available size which parent could give to this view depends on aspect ratio. - * - * @return True if wrapping to content. - */ - public boolean isWrapToContent() { - return wrapToContent; - } - - /** - * Sets if layout is wrapping to content but holds aspect ratio. - * - * @param wrapToContent True if wrapping to content. - */ - public void setWrapToContent(final boolean wrapToContent) { - if (wrapToContent == this.wrapToContent) { - return; - } - - this.wrapToContent = wrapToContent; - requestLayout(); - } - - private void setMeasuredDimensionWithAspectOfLesser(final int measuredWidth, final int measuredHeight) { - final float heightBasedOnMw = measuredWidth / aspectRatio; - if (heightBasedOnMw > measuredHeight) { - setMeasuredDimension((int) (measuredHeight * aspectRatio), measuredHeight); - } else { - setMeasuredDimension(measuredWidth, (int) heightBasedOnMw); - } - } - - private void setMeasuredDimensionWithAspectOfHigher(final int measuredWidth, final int measuredHeight) { - final float heightBasedOnMw = measuredWidth / aspectRatio; - if (heightBasedOnMw < measuredHeight) { - setMeasuredDimension((int) (measuredHeight * aspectRatio), measuredHeight); - } else { - setMeasuredDimension(measuredWidth, (int) heightBasedOnMw); - } - } - - @NonNull - private Point measureWrapChildren(final int widthMeasureSpec, final int heightMeasureSpec) { - final Point result = new Point(); - for (int i = 0; i < getChildCount(); i++) { - final View child = getChildAt(i); - child.measure(widthMeasureSpec, heightMeasureSpec); - if (result.x < child.getMeasuredWidth()) { - result.x = child.getMeasuredWidth(); - } - if (result.y < child.getMeasuredHeight()) { - result.y = child.getMeasuredHeight(); - } - } - return result; - } - - @Override - protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - final int widthMode = MeasureSpec.getMode(widthMeasureSpec); - final int heightMode = MeasureSpec.getMode(heightMeasureSpec); - - if (wrapToContent) { - final Point bounds = measureWrapChildren(widthMeasureSpec, heightMeasureSpec); - width = widthMode == MeasureSpec.UNSPECIFIED ? bounds.x : Math.min(bounds.x, width); - height = heightMode == MeasureSpec.UNSPECIFIED ? bounds.y : Math.min(bounds.y, height); - } - - if (widthMode == MeasureSpec.UNSPECIFIED) { - if (heightMode == MeasureSpec.UNSPECIFIED) { - measureBothUnspecified(width, height); - } else { - measureOnlyUnspecifiedWidth(width, height); - } - } else if (heightMode == MeasureSpec.UNSPECIFIED) { - measureOnlyUnspecifiedHeight(width, height); - } else { - measureBothSpecified(width, height); - } - } - - private void measureBothSpecified(final int width, final int height) { - if (wrapToContent) { - setMeasuredDimensionWithAspectOfHigher(width, height); - } else { - setMeasuredDimensionWithAspectOfLesser(width, height); - } - } - - private void measureOnlyUnspecifiedHeight(final int width, final int height) { - if (wrapToContent) { - measureWrapToContent(width, height); - } else { - setMeasuredDimension(width, (int) (width / aspectRatio)); - } - } - - private void measureWrapToContent(final int width, final int height) { - if (width < (int) (height * aspectRatio)) { - setMeasuredDimension((int) (height * aspectRatio), height); - } else { - setMeasuredDimension(width, (int) (width / aspectRatio)); - } - } - - private void measureOnlyUnspecifiedWidth(final int width, final int height) { - if (wrapToContent) { - measureWrapToContent(width, height); - } else { - setMeasuredDimension((int) (height * aspectRatio), height); - } - } - - private void measureBothUnspecified(final int width, final int height) { - if (wrapToContent) { - setMeasuredDimensionWithAspectOfHigher(width, height); - } else { - final DisplayMetrics metrics = getResources().getDisplayMetrics(); - setMeasuredDimensionWithAspectOfLesser(metrics.widthPixels, metrics.heightPixels); - } - } - - @Override - protected void onLayout(final boolean changed, final int left, final int top, final int right, final int bottom) { - for (int i = 0; i < getChildCount(); i++) { - final View child = getChildAt(i); - final ViewGroup.LayoutParams lp = child.getLayoutParams(); - final int widthMeasureSpec; - final int heightMeasureSpec; - final int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); - final int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); - switch (lp.width) { - case ViewGroup.LayoutParams.MATCH_PARENT: - widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); - break; - case ViewGroup.LayoutParams.WRAP_CONTENT: - widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST); - break; - default: - widthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); - break; - } - - switch (lp.height) { - case ViewGroup.LayoutParams.MATCH_PARENT: - heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); - break; - case ViewGroup.LayoutParams.WRAP_CONTENT: - heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST); - break; - default: - heightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); - break; - } - - getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); - } - - super.onLayout(changed, left, top, right, bottom); - } - -} \ No newline at end of file diff --git a/src/main/java/ru/touchin/roboswag/components/views/LifecycleView.java b/src/main/java/ru/touchin/roboswag/components/views/LifecycleView.java deleted file mode 100644 index eafcc48..0000000 --- a/src/main/java/ru/touchin/roboswag/components/views/LifecycleView.java +++ /dev/null @@ -1,284 +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.views; - - -import android.content.Context; -import android.os.Parcelable; -import android.support.annotation.AttrRes; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.widget.FrameLayout; - -import ru.touchin.roboswag.components.utils.BaseLifecycleBindable; -import ru.touchin.roboswag.components.utils.LifecycleBindable; -import rx.Completable; -import rx.Observable; -import rx.Single; -import rx.Subscription; -import rx.functions.Action0; -import rx.functions.Action1; - - -/** - * Created by Gavriil Sitnikov on 18/05/17. - * FrameLayout that realizes LifecycleBindable interface. - */ -@SuppressWarnings({"CPD-START", "PMD.TooManyMethods"}) -public class LifecycleView extends FrameLayout implements LifecycleBindable { - - @NonNull - private final BaseLifecycleBindable baseLifecycleBindable; - private boolean created; - private boolean started; - - public LifecycleView(@NonNull final Context context) { - super(context); - baseLifecycleBindable = new BaseLifecycleBindable(); - } - - public LifecycleView(@NonNull final Context context, @Nullable final AttributeSet attrs) { - super(context, attrs); - baseLifecycleBindable = new BaseLifecycleBindable(); - } - - public LifecycleView(@NonNull final Context context, @Nullable final AttributeSet attrs, @AttrRes final int defStyleAttr) { - super(context, attrs, defStyleAttr); - baseLifecycleBindable = new BaseLifecycleBindable(); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - onCreate(); - if (!started && getWindowSystemUiVisibility() == VISIBLE) { - onStart(); - } - } - - /** - * Calls when view attached to window and ready to use. - */ - protected void onCreate() { - created = true; - baseLifecycleBindable.onCreate(); - } - - /** - * Calls when view's window showed or state restored. - */ - protected void onStart() { - started = true; - baseLifecycleBindable.onStart(); - } - - @Override - protected void onRestoreInstanceState(@NonNull final Parcelable state) { - super.onRestoreInstanceState(state); - if (created && !started) { - onStart(); - } - } - - @NonNull - @Override - protected Parcelable onSaveInstanceState() { - started = false; - baseLifecycleBindable.onSaveInstanceState(); - return super.onSaveInstanceState(); - } - - /** - * Calls when view's window hided or state saved. - */ - protected void onStop() { - started = false; - baseLifecycleBindable.onStop(); - } - - /** - * Calls when view detached from window. - */ - protected void onDestroy() { - if (started) { - onStop(); - } - created = false; - baseLifecycleBindable.onDestroy(); - } - - @Override - protected void onDetachedFromWindow() { - onDestroy(); - super.onDetachedFromWindow(); - } - - @Override - protected void onWindowVisibilityChanged(final int visibility) { - super.onWindowVisibilityChanged(visibility); - if (visibility == VISIBLE) { - if (created && !started) { - baseLifecycleBindable.onStart(); - } - } else if (started) { - baseLifecycleBindable.onStop(); - } - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable) { - return baseLifecycleBindable.untilStop(observable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilStop(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single) { - return baseLifecycleBindable.untilStop(single); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return baseLifecycleBindable.untilStop(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable) { - return baseLifecycleBindable.untilStop(completable); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilStop(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilStop(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilStop(completable, onCompletedAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable) { - return baseLifecycleBindable.untilDestroy(observable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, @NonNull final Action1 onNextAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Observable observable, - @NonNull final Action1 onNextAction, - @NonNull final Action1 onErrorAction, - @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilDestroy(observable, onNextAction, onErrorAction, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single) { - return baseLifecycleBindable.untilDestroy(single); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, @NonNull final Action1 onSuccessAction) { - return baseLifecycleBindable.untilDestroy(single, onSuccessAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Single single, - @NonNull final Action1 onSuccessAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(single, onSuccessAction, onErrorAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable) { - return baseLifecycleBindable.untilDestroy(completable); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, @NonNull final Action0 onCompletedAction) { - return baseLifecycleBindable.untilDestroy(completable, onCompletedAction); - } - - @NonNull - @Override - public Subscription untilDestroy(@NonNull final Completable completable, - @NonNull final Action0 onCompletedAction, - @NonNull final Action1 onErrorAction) { - return baseLifecycleBindable.untilDestroy(completable, onCompletedAction, onErrorAction); - } - -} diff --git a/src/main/res/anim/global_fade_in_animation.xml b/src/main/res/anim/global_fade_in_animation.xml deleted file mode 100644 index 200a2bf..0000000 --- a/src/main/res/anim/global_fade_in_animation.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_fade_out_animation.xml b/src/main/res/anim/global_fade_out_animation.xml deleted file mode 100644 index 472546a..0000000 --- a/src/main/res/anim/global_fade_out_animation.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_slide_in_left_animation.xml b/src/main/res/anim/global_slide_in_left_animation.xml deleted file mode 100644 index 9ddb859..0000000 --- a/src/main/res/anim/global_slide_in_left_animation.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_slide_in_right_animation.xml b/src/main/res/anim/global_slide_in_right_animation.xml deleted file mode 100644 index ac7f27a..0000000 --- a/src/main/res/anim/global_slide_in_right_animation.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_slide_out_left_animation.xml b/src/main/res/anim/global_slide_out_left_animation.xml deleted file mode 100644 index 5bc1921..0000000 --- a/src/main/res/anim/global_slide_out_left_animation.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/anim/global_slide_out_right_animation.xml b/src/main/res/anim/global_slide_out_right_animation.xml deleted file mode 100644 index 0839498..0000000 --- a/src/main/res/anim/global_slide_out_right_animation.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - \ No newline at end of file diff --git a/src/main/res/drawable-v21/global_dark_selector.xml b/src/main/res/drawable-v21/global_dark_selector.xml deleted file mode 100644 index 847128c..0000000 --- a/src/main/res/drawable-v21/global_dark_selector.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/main/res/drawable-v21/global_light_selector.xml b/src/main/res/drawable-v21/global_light_selector.xml deleted file mode 100644 index b0932fd..0000000 --- a/src/main/res/drawable-v21/global_light_selector.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/main/res/drawable/global_dark_selector.xml b/src/main/res/drawable/global_dark_selector.xml deleted file mode 100644 index 1a825ed..0000000 --- a/src/main/res/drawable/global_dark_selector.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/main/res/drawable/global_light_selector.xml b/src/main/res/drawable/global_light_selector.xml deleted file mode 100644 index 39c33f0..0000000 --- a/src/main/res/drawable/global_light_selector.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/storable/.gitignore b/storable/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/storable/.gitignore @@ -0,0 +1 @@ +/build diff --git a/storable/build.gradle b/storable/build.gradle new file mode 100644 index 0000000..18cb730 --- /dev/null +++ b/storable/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":utils") + api project(":logging") + + implementation "com.android.support:support-annotations:$versions.supportLibrary" + + implementation "io.reactivex.rxjava2:rxjava:$versions.rxJava" + implementation "io.reactivex.rxjava2:rxandroid:$versions.rxAndroid" +} diff --git a/storable/src/main/AndroidManifest.xml b/storable/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fd2722a --- /dev/null +++ b/storable/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java similarity index 56% rename from src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java rename to storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java index d7e33a2..3349dc9 100644 --- a/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java +++ b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceStore.java @@ -25,12 +25,11 @@ import android.support.annotation.Nullable; import java.lang.reflect.Type; +import io.reactivex.Completable; +import io.reactivex.Single; import ru.touchin.roboswag.core.log.Lc; import ru.touchin.roboswag.core.observables.storable.Store; import ru.touchin.roboswag.core.utils.Optional; -import rx.Completable; -import rx.Single; - /** * Created by Gavriil Sitnikov on 18/03/16. @@ -72,52 +71,54 @@ public class PreferenceStore implements Store { @NonNull @Override public Completable storeObject(@NonNull final Type storeObjectType, @NonNull final String key, @Nullable final T storeObject) { - return Completable.fromAction(() -> { - if (storeObject == null) { - preferences.edit().remove(key).apply(); - return; - } - - if (isTypeBoolean(storeObjectType)) { - preferences.edit().putBoolean(key, (Boolean) storeObject).apply(); - } else if (storeObjectType.equals(String.class)) { - preferences.edit().putString(key, (String) storeObject).apply(); - } else if (isTypeInteger(storeObjectType)) { - preferences.edit().putInt(key, (Integer) storeObject).apply(); - } else if (isTypeLong(storeObjectType)) { - preferences.edit().putLong(key, (Long) storeObject).apply(); - } else if (isTypeFloat(storeObjectType)) { - preferences.edit().putFloat(key, (Float) storeObject).apply(); - } else { - Lc.assertion("Unsupported type of object " + storeObjectType); - } - }); + return Completable.fromAction(() -> setObject(storeObjectType, key, storeObject)); } @NonNull @Override - @SuppressWarnings("unchecked") - //unchecked: it is checking class in if-else statements public Single> loadObject(@NonNull final Type storeObjectType, @NonNull final String key) { - return Single.fromCallable(() -> { - if (!preferences.contains(key)) { - return new Optional<>(null); - } + return Single.fromCallable(() -> new Optional<>(!preferences.contains(key) ? null : getObject(storeObjectType, key))); + } - if (isTypeBoolean(storeObjectType)) { - return new Optional<>((T) ((Boolean) preferences.getBoolean(key, false))); - } else if (storeObjectType.equals(String.class)) { - return new Optional<>((T) (preferences.getString(key, null))); - } else if (isTypeInteger(storeObjectType)) { - return new Optional<>((T) ((Integer) preferences.getInt(key, 0))); - } else if (isTypeLong(storeObjectType)) { - return new Optional<>((T) ((Long) preferences.getLong(key, 0L))); - } else if (isTypeFloat(storeObjectType)) { - return new Optional<>((T) ((Float) preferences.getFloat(key, 0f))); - } + @Override + public void setObject(@NonNull final Type storeObjectType, @NonNull final String key, @Nullable final T storeObject) { + if (storeObject == null) { + preferences.edit().remove(key).apply(); + return; + } + if (isTypeBoolean(storeObjectType)) { + preferences.edit().putBoolean(key, (Boolean) storeObject).apply(); + } else if (storeObjectType.equals(String.class)) { + preferences.edit().putString(key, (String) storeObject).apply(); + } else if (isTypeInteger(storeObjectType)) { + preferences.edit().putInt(key, (Integer) storeObject).apply(); + } else if (isTypeLong(storeObjectType)) { + preferences.edit().putLong(key, (Long) storeObject).apply(); + } else if (isTypeFloat(storeObjectType)) { + preferences.edit().putFloat(key, (Float) storeObject).apply(); + } else { Lc.assertion("Unsupported type of object " + storeObjectType); - return new Optional<>(null); - }); + } + } + + @Nullable + @SuppressWarnings("unchecked") //unchecked: it is checking class in if-else statements + @Override + public T getObject(@NonNull final Type storeObjectType, @NonNull final String key) { + if (isTypeBoolean(storeObjectType)) { + return (T) ((Boolean) preferences.getBoolean(key, false)); + } else if (storeObjectType.equals(String.class)) { + return (T) (preferences.getString(key, null)); + } else if (isTypeInteger(storeObjectType)) { + return (T) ((Integer) preferences.getInt(key, 0)); + } else if (isTypeLong(storeObjectType)) { + return (T) ((Long) preferences.getLong(key, 0L)); + } else if (isTypeFloat(storeObjectType)) { + return (T) ((Float) preferences.getFloat(key, 0f)); + } else { + Lc.assertion("Unsupported type of object " + storeObjectType); + return null; + } } } diff --git a/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java similarity index 61% rename from src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java rename to storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java index 6d710b5..b9e9883 100644 --- a/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java +++ b/storable/src/main/java/ru/touchin/roboswag/components/utils/storables/PreferenceUtils.java @@ -28,7 +28,7 @@ import java.lang.reflect.Type; import ru.touchin.roboswag.core.observables.storable.Converter; import ru.touchin.roboswag.core.observables.storable.SameTypesConverter; import ru.touchin.roboswag.core.observables.storable.Storable; -import ru.touchin.roboswag.core.observables.storable.concrete.NonNullStorable; +import ru.touchin.roboswag.core.observables.storable.NonNullStorable; /** * Created by Gavriil Sitnikov on 01/09/2016. @@ -45,9 +45,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable stringStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, String.class, - String.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + String.class, + String.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -59,13 +63,18 @@ public final class PreferenceUtils { * @return {@link Storable} for string. */ @NonNull - public static NonNullStorable stringStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - @NonNull final String defaultValue) { - return new Storable.Builder(name, String.class, - String.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable stringStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + @NonNull final String defaultValue + ) { + return new Storable.Builder( + name, + String.class, + String.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -77,9 +86,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable longStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, Long.class, - Long.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + Long.class, + Long.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -91,13 +104,18 @@ public final class PreferenceUtils { * @return {@link Storable} for long. */ @NonNull - public static NonNullStorable longStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - final long defaultValue) { - return new Storable.Builder(name, Long.class, - Long.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable longStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + final long defaultValue + ) { + return new Storable.Builder( + name, + Long.class, + Long.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -109,9 +127,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable booleanStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, Boolean.class, - Boolean.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + Boolean.class, + Boolean.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -123,13 +145,18 @@ public final class PreferenceUtils { * @return {@link Storable} for boolean. */ @NonNull - public static NonNullStorable booleanStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - final boolean defaultValue) { - return new Storable.Builder(name, Boolean.class, - Boolean.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable booleanStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + final boolean defaultValue + ) { + return new Storable.Builder( + name, + Boolean.class, + Boolean.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -141,9 +168,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable integerStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, Integer.class, - Integer.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + Integer.class, + Integer.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -155,13 +186,18 @@ public final class PreferenceUtils { * @return {@link Storable} for integer. */ @NonNull - public static NonNullStorable integerStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - final int defaultValue) { - return new Storable.Builder(name, Integer.class, - Integer.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable integerStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + final int defaultValue + ) { + return new Storable.Builder( + name, + Integer.class, + Integer.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -173,9 +209,13 @@ public final class PreferenceUtils { */ @NonNull public static Storable floatStorable(@NonNull final String name, @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, Float.class, - Float.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .build(); + return new Storable.Builder( + name, + Float.class, + Float.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).build(); } /** @@ -187,13 +227,18 @@ public final class PreferenceUtils { * @return {@link Storable} for float. */ @NonNull - public static NonNullStorable floatStorable(@NonNull final String name, - @NonNull final SharedPreferences preferences, - final float defaultValue) { - return new Storable.Builder(name, Float.class, - Float.class, new PreferenceStore<>(preferences), new SameTypesConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static NonNullStorable floatStorable( + @NonNull final String name, + @NonNull final SharedPreferences preferences, + final float defaultValue + ) { + return new Storable.Builder( + name, + Float.class, + Float.class, + new PreferenceStore<>(preferences), + new SameTypesConverter<>() + ).setDefaultValue(defaultValue).build(); } /** @@ -204,12 +249,18 @@ public final class PreferenceUtils { * @return {@link Storable} for enum. */ @NonNull - public static > Storable enumStorable(@NonNull final String name, - @NonNull final Class enumClass, - @NonNull final SharedPreferences preferences) { - return new Storable.Builder(name, enumClass, - String.class, new PreferenceStore<>(preferences), new EnumToStringConverter<>()) - .build(); + public static > Storable enumStorable( + @NonNull final String name, + @NonNull final Class enumClass, + @NonNull final SharedPreferences preferences + ) { + return new Storable.Builder( + name, + enumClass, + String.class, + new PreferenceStore<>(preferences), + new EnumToStringConverter<>() + ).build(); } /** @@ -221,14 +272,19 @@ public final class PreferenceUtils { * @return {@link Storable} for enum. */ @NonNull - public static > NonNullStorable enumStorable(@NonNull final String name, - @NonNull final Class enumClass, - @NonNull final SharedPreferences preferences, - @NonNull final T defaultValue) { - return new Storable.Builder(name, enumClass, - String.class, new PreferenceStore<>(preferences), new EnumToStringConverter<>()) - .setDefaultValue(defaultValue) - .build(); + public static > NonNullStorable enumStorable( + @NonNull final String name, + @NonNull final Class enumClass, + @NonNull final SharedPreferences preferences, + @NonNull final T defaultValue + ) { + return new Storable.Builder( + name, + enumClass, + String.class, + new PreferenceStore<>(preferences), + new EnumToStringConverter<>() + ).setDefaultValue(defaultValue).build(); } private PreferenceUtils() { diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/ObservableRefCountWithCacheTime.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/ObservableRefCountWithCacheTime.java new file mode 100644 index 0000000..6217204 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/ObservableRefCountWithCacheTime.java @@ -0,0 +1,299 @@ +/* + Copyright (c) 2016-present, RxJava Contributors. +

+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at +

+ http://www.apache.org/licenses/LICENSE-2.0 +

+ Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + the License for the specific language governing permissions and limitations under the License. + */ + +package ru.touchin.roboswag.core.observables; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import io.reactivex.Observable; +import io.reactivex.ObservableSource; +import io.reactivex.Observer; +import io.reactivex.Scheduler; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.disposables.Disposables; +import io.reactivex.functions.Consumer; +import io.reactivex.internal.disposables.DisposableHelper; +import io.reactivex.internal.fuseable.HasUpstreamObservableSource; +import io.reactivex.observables.ConnectableObservable; +import io.reactivex.schedulers.Schedulers; + +/** + * Returns an observable sequence that stays connected to the source as long as + * there is at least one subscription to the observable sequence. + * + * @param the value type + */ +@SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.AvoidUsingVolatile"}) +//AvoidUsingVolatile: it's RxJava code +public final class ObservableRefCountWithCacheTime extends Observable implements HasUpstreamObservableSource { + + @NonNull + private final ConnectableObservable connectableSource; + @NonNull + private final ObservableSource actualSource; + + @NonNull + private volatile CompositeDisposable baseDisposable = new CompositeDisposable(); + + @NonNull + private final AtomicInteger subscriptionCount = new AtomicInteger(); + + /** + * Use this lock for every subscription and disconnect action. + */ + @NonNull + private final ReentrantLock lock = new ReentrantLock(); + + @NonNull + private final Scheduler scheduler = Schedulers.computation(); + private final long cacheTime; + @NonNull + private final TimeUnit cacheTimeUnit; + @Nullable + private Scheduler.Worker worker; + + /** + * Constructor. + * + * @param source observable to apply ref count to + */ + public ObservableRefCountWithCacheTime(@NonNull final ConnectableObservable source, + final long cacheTime, @NonNull final TimeUnit cacheTimeUnit) { + super(); + this.connectableSource = source; + this.actualSource = source; + this.cacheTime = cacheTime; + this.cacheTimeUnit = cacheTimeUnit; + } + + @NonNull + public ObservableSource source() { + return actualSource; + } + + private void cleanupWorker() { + if (worker != null) { + worker.dispose(); + worker = null; + } + } + + @Override + public void subscribeActual(@NonNull final Observer subscriber) { + + lock.lock(); + if (subscriptionCount.incrementAndGet() == 1) { + cleanupWorker(); + final AtomicBoolean writeLocked = new AtomicBoolean(true); + + try { + // need to use this overload of connect to ensure that + // baseDisposable is set in the case that source is a + // synchronous Observable + connectableSource.connect(onSubscribe(subscriber, writeLocked)); + } finally { + // need to cover the case where the source is subscribed to + // outside of this class thus preventing the Action1 passed + // to source.connect above being called + if (writeLocked.get()) { + // Action1 passed to source.connect was not called + lock.unlock(); + } + } + } else { + try { + // ready to subscribe to source so do it + doSubscribe(subscriber, baseDisposable); + } finally { + // release the read lock + lock.unlock(); + } + } + + } + + @NonNull + private Consumer onSubscribe(@NonNull final Observer observer, @NonNull final AtomicBoolean writeLocked) { + return new DisposeConsumer(observer, writeLocked); + } + + private void doSubscribe(@NonNull final Observer observer, @NonNull final CompositeDisposable currentBase) { + // handle disposing from the base CompositeDisposable + final Disposable disposable = disconnect(currentBase); + + final ConnectionObserver connectionObserver = new ConnectionObserver(observer, currentBase, disposable); + observer.onSubscribe(connectionObserver); + + connectableSource.subscribe(connectionObserver); + } + + @NonNull + private Disposable disconnect(@NonNull final CompositeDisposable current) { + return Disposables.fromRunnable(new DisposeTask(current)); + } + + private final class ConnectionObserver extends AtomicReference implements Observer, Disposable { + + private static final long serialVersionUID = 3813126992133394324L; + + @NonNull + private final Observer subscriber; + @NonNull + private final CompositeDisposable currentBase; + @NonNull + private final Disposable resource; + + public ConnectionObserver(@NonNull final Observer subscriber, @NonNull final CompositeDisposable currentBase, + @NonNull final Disposable resource) { + super(); + this.subscriber = subscriber; + this.currentBase = currentBase; + this.resource = resource; + } + + @Override + public void onSubscribe(@NonNull final Disposable disposable) { + DisposableHelper.setOnce(this, disposable); + } + + @Override + public void onError(@NonNull final Throwable throwable) { + cleanup(); + subscriber.onError(throwable); + } + + @Override + public void onNext(@NonNull final T item) { + subscriber.onNext(item); + } + + @Override + public void onComplete() { + cleanup(); + subscriber.onComplete(); + } + + @Override + public void dispose() { + DisposableHelper.dispose(this); + resource.dispose(); + } + + @Override + public boolean isDisposed() { + return DisposableHelper.isDisposed(get()); + } + + private void cleanup() { + // on error or completion we need to dispose the base CompositeDisposable + // and set the subscriptionCount to 0 + lock.lock(); + try { + if (baseDisposable == currentBase) { + cleanupWorker(); + if (connectableSource instanceof Disposable) { + ((Disposable) connectableSource).dispose(); + } + + baseDisposable.dispose(); + baseDisposable = new CompositeDisposable(); + subscriptionCount.set(0); + } + } finally { + lock.unlock(); + } + } + + } + + private final class DisposeConsumer implements Consumer { + + @NonNull + private final Observer observer; + @NonNull + private final AtomicBoolean writeLocked; + + public DisposeConsumer(@NonNull final Observer observer, @NonNull final AtomicBoolean writeLocked) { + this.observer = observer; + this.writeLocked = writeLocked; + } + + @Override + public void accept(@NonNull final Disposable subscription) { + try { + baseDisposable.add(subscription); + // ready to subscribe to source so do it + doSubscribe(observer, baseDisposable); + } finally { + // release the write lock + lock.unlock(); + writeLocked.set(false); + } + } + + } + + private final class DisposeTask implements Runnable { + + @NonNull + private final CompositeDisposable current; + + public DisposeTask(@NonNull final CompositeDisposable current) { + this.current = current; + } + + @Override + public void run() { + lock.lock(); + try { + if (baseDisposable == current && subscriptionCount.decrementAndGet() == 0) { + if (worker != null) { + worker.dispose(); + } else { + worker = scheduler.createWorker(); + } + worker.schedule(() -> { + lock.lock(); + try { + if (subscriptionCount.get() == 0) { + cleanupWorker(); + if (connectableSource instanceof Disposable) { + ((Disposable) connectableSource).dispose(); + } + + baseDisposable.dispose(); + // need a new baseDisposable because once + // disposed stays that way + baseDisposable = new CompositeDisposable(); + } + } finally { + lock.unlock(); + } + }, cacheTime, cacheTimeUnit); + } + } finally { + lock.unlock(); + } + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/BaseStorable.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/BaseStorable.java new file mode 100644 index 0000000..7b721b4 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/BaseStorable.java @@ -0,0 +1,485 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Completable; +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import io.reactivex.Single; +import io.reactivex.plugins.RxJavaPlugins; +import io.reactivex.schedulers.Schedulers; +import io.reactivex.subjects.PublishSubject; +import ru.touchin.roboswag.core.log.LcGroup; +import ru.touchin.roboswag.core.observables.ObservableRefCountWithCacheTime; +import ru.touchin.roboswag.core.utils.ObjectUtils; +import ru.touchin.roboswag.core.utils.Optional; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Base class allows to async access to some store. + * Supports conversion between store and actual value. If it is not needed then use {@link SameTypesConverter} + * Supports migration from specific version to latest by {@link Migration} object. + * Allows to set default value which will be returned if actual value is null. + * Allows to declare specific {@link ObserveStrategy}. + * Also specific {@link Scheduler} could be specified to not create new scheduler per storable. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}; + * @param Type of actual value operating by Storable. Could be same as {@link TObject}. + */ +public abstract class BaseStorable { + + public static final LcGroup STORABLE_LC_GROUP = new LcGroup("STORABLE"); + + private static final long DEFAULT_CACHE_TIME_MILLIS = TimeUnit.SECONDS.toMillis(5); + + @NonNull + private static ObserveStrategy getDefaultObserveStrategyFor(@NonNull final Type objectType, @NonNull final Type storeObjectType) { + if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) objectType)) { + return ObserveStrategy.CACHE_ACTUAL_VALUE; + } + if (objectType instanceof Class && ObjectUtils.isSimpleClass((Class) storeObjectType)) { + return ObserveStrategy.CACHE_STORE_VALUE; + } + return ObserveStrategy.NO_CACHE; + } + + @NonNull + private final TKey key; + @NonNull + private final Type objectType; + @NonNull + private final Type storeObjectType; + @NonNull + private final Store store; + @NonNull + private final Converter converter; + @NonNull + private final PublishSubject> newStoreValueEvent = PublishSubject.create(); + @NonNull + private final Observable> storeValueObservable; + @NonNull + private final Observable> valueObservable; + @NonNull + private final Scheduler scheduler; + + public BaseStorable(@NonNull final BuilderCore builderCore) { + this(builderCore.key, builderCore.objectType, builderCore.storeObjectType, + builderCore.store, builderCore.converter, builderCore.observeStrategy, + builderCore.migration, builderCore.defaultValue, builderCore.storeScheduler, builderCore.cacheTimeMillis); + } + + @SuppressWarnings("PMD.ExcessiveParameterList") + //ExcessiveParameterList: that's why we are using builder to create it + private BaseStorable(@NonNull final TKey key, + @NonNull final Type objectType, + @NonNull final Type storeObjectType, + @NonNull final Store store, + @NonNull final Converter converter, + @Nullable final ObserveStrategy observeStrategy, + @Nullable final Migration migration, + @Nullable final TObject defaultValue, + @Nullable final Scheduler storeScheduler, + final long cacheTimeMillis) { + this.key = key; + this.objectType = objectType; + this.storeObjectType = storeObjectType; + this.store = store; + this.converter = converter; + + final ObserveStrategy nonNullObserveStrategy + = observeStrategy != null ? observeStrategy : getDefaultObserveStrategyFor(objectType, storeObjectType); + scheduler = storeScheduler != null ? storeScheduler : Schedulers.from(Executors.newSingleThreadExecutor()); + storeValueObservable + = createStoreValueObservable(nonNullObserveStrategy, migration, defaultValue, cacheTimeMillis); + valueObservable = createValueObservable(storeValueObservable, nonNullObserveStrategy, cacheTimeMillis); + } + + @Nullable + private Optional returnDefaultValueIfNull(@NonNull final Optional storeObject, @Nullable final TObject defaultValue) + throws Converter.ConversionException { + if (storeObject.get() != null || defaultValue == null) { + return storeObject; + } + + try { + return new Optional<>(converter.toStoreObject(objectType, storeObjectType, defaultValue)); + } catch (final Converter.ConversionException exception) { + STORABLE_LC_GROUP.w(exception, "Exception while converting default value of '%s' from '%s' from store %s", + key, defaultValue, store); + throw exception; + } + } + + @NonNull + private Observable> createStoreInitialLoadingObservable(@Nullable final Migration migration) { + final Single> loadObservable = store.loadObject(storeObjectType, key) + .doOnError(throwable -> STORABLE_LC_GROUP.w(throwable, "Exception while trying to load value of '%s' from store %s", key, store)); + return (migration != null ? migration.migrateToLatestVersion(key).andThen(loadObservable) : loadObservable) + .subscribeOn(scheduler) + .observeOn(scheduler) + .toObservable() + .replay(1) + .refCount() + .take(1); + } + + @NonNull + private Observable> createStoreValueObservable(@NonNull final ObserveStrategy observeStrategy, + @Nullable final Migration migration, + @Nullable final TObject defaultValue, + final long cacheTimeMillis) { + final Observable> storeInitialLoadingObservable = createStoreInitialLoadingObservable(migration); + final Observable> result = storeInitialLoadingObservable + .concatWith(newStoreValueEvent) + .map(storeObject -> returnDefaultValueIfNull(storeObject, defaultValue)); + return observeStrategy == ObserveStrategy.CACHE_STORE_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE + ? RxJavaPlugins.onAssembly(new ObservableRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS)) + : result; + } + + @NonNull + private Observable> createValueObservable(@NonNull final Observable> storeValueObservable, + @NonNull final ObserveStrategy observeStrategy, + final long cacheTimeMillis) { + final Observable> result = storeValueObservable + .map(storeObject -> { + try { + return new Optional<>(converter.toObject(objectType, storeObjectType, storeObject.get())); + } catch (final Converter.ConversionException exception) { + STORABLE_LC_GROUP.w(exception, "Exception while trying to converting value of '%s' from store %s by %s", + key, storeObject, store, converter); + throw exception; + } + }); + return observeStrategy == ObserveStrategy.CACHE_ACTUAL_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE + ? RxJavaPlugins.onAssembly(new ObservableRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS)) + : result; + } + + /** + * Returns key of value. + * + * @return Unique key. + */ + @NonNull + public TKey getKey() { + return key; + } + + /** + * Returns type of actual object. + * + * @return Type of actual object. + */ + @NonNull + public Type getObjectType() { + return objectType; + } + + /** + * Returns type of store object. + * + * @return Type of store object. + */ + @NonNull + public Type getStoreObjectType() { + return storeObjectType; + } + + /** + * Returns {@link Store} where store class representation of object is storing. + * + * @return Store. + */ + @NonNull + public Store getStore() { + return store; + } + + /** + * Returns {@link Converter} to convert values from store class to actual and back. + * + * @return Converter. + */ + @NonNull + public Converter getConverter() { + return converter; + } + + @NonNull + private Completable internalSet(@Nullable final TObject newValue, final boolean checkForEqualityBeforeSet) { + return (checkForEqualityBeforeSet ? storeValueObservable.firstOrError() : Single.just(new Optional<>(null))) + .observeOn(scheduler) + .flatMapCompletable(oldStoreValue -> { + final TStoreObject newStoreValue; + try { + newStoreValue = converter.toStoreObject(objectType, storeObjectType, newValue); + } catch (final Converter.ConversionException exception) { + STORABLE_LC_GROUP.w(exception, "Exception while trying to store value of '%s' from store %s by %s", + key, newValue, store, converter); + return Completable.error(exception); + } + if (checkForEqualityBeforeSet && ObjectUtils.equals(newStoreValue, oldStoreValue.get())) { + return Completable.complete(); + } + return store.storeObject(storeObjectType, key, newStoreValue) + .doOnError(throwable -> STORABLE_LC_GROUP.w(throwable, + "Exception while trying to store value of '%s' from store %s by %s", + key, newValue, store, converter)) + .observeOn(scheduler) + .andThen(Completable.fromAction(() -> { + newStoreValueEvent.onNext(new Optional<>(newStoreValue)); + if (checkForEqualityBeforeSet) { + STORABLE_LC_GROUP.i("Value of '%s' changed from '%s' to '%s'", key, oldStoreValue, newStoreValue); + } else { + STORABLE_LC_GROUP.i("Value of '%s' force changed to '%s'", key, newStoreValue); + } + })); + }); + } + + /** + * Creates observable which is async setting value to store. + * It is not checking if stored value equals new value. + * In result it will be faster to not get value from store and compare but it will emit item to {@link #observe()} subscribers. + * NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! + * + * @param newValue Value to set; + * @return Observable of setting process. + */ + @NonNull + public Completable forceSet(@Nullable final TObject newValue) { + return internalSet(newValue, false); + } + + /** + * Creates observable which is async setting value to store. + * It is checking if stored value equals new value. + * In result it will take time to get value from store and compare + * but it won't emit item to {@link #observe()} subscribers if stored value equals new value. + * NOTE: It could emit ONLY completed and errors events. It is not providing onNext event! + * + * @param newValue Value to set; + * @return Observable of setting process. + */ + @NonNull + public Completable set(@Nullable final TObject newValue) { + return internalSet(newValue, true); + } + + /** + * Sets value synchronously. You should NOT use this method normally. Use {@link #set(Object)} asynchronously instead. + * + * @param newValue Value to set; + */ + public void setSync(@Nullable final TObject newValue) { + 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; + } + store.setObject(storeObjectType, key, newStoreValue); + newStoreValueEvent.onNext(new Optional<>(newStoreValue)); + STORABLE_LC_GROUP.i("Value of '%s' force changed to '%s'", key, newStoreValue); + } + + @NonNull + protected Observable> observeOptionalValue() { + return valueObservable; + } + + /** + * Returns Observable which is emitting item on subscribe and every time when someone have changed value. + * It could emit next and error events but not completed. + * + * @return Returns observable of value. + */ + @NonNull + public abstract Observable observe(); + + /** + * Returns Observable which is emitting only one item on subscribe. + * It could emit next and error events but not completed. + * + * @return Returns observable of value. + */ + @NonNull + public Single get() { + return observe().firstOrError(); + } + + /** + * Gets value synchronously. You should NOT use this method normally. Use {@link #get()} or {@link #observe()} asynchronously instead. + * + * @return Returns value; + */ + @Nullable + public TObject getSync() { + final TStoreObject storeObject = store.getObject(storeObjectType, key); + try { + return converter.toObject(objectType, storeObjectType, storeObject); + } 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); + return null; + } + } + + /** + * Enum that is representing strategy of observing item from store. + */ + public enum ObserveStrategy { + + /** + * Not caching value so on every {@link #get()} emit it will get value from {@link #getStore()} and converts it with {@link #getConverter()}. + */ + NO_CACHE, + /** + * Caching only store value so on every {@link #get()} emit it will converts it with {@link #getConverter()}. + * Do not use such strategy if store object could be big (like byte-array of file). + */ + CACHE_STORE_VALUE, + /** + * Caching value so it won't spend time for getting value from {@link #getStore()} and converts it by {@link #getConverter()}. + * But it will take time for getting value from {@link #getStore()} to set value. + * Do not use such strategy if object could be big (like Bitmap or long string). + * Do not use such strategy if object is mutable because multiple subscribers could then change it's state. + */ + CACHE_ACTUAL_VALUE, + /** + * Caching value so it won't spend time for getting value from {@link #getStore()} and converts it by {@link #getConverter()}. + * It won't take time or getting value from {@link #getStore()} to set value. + * Do not use such strategy if store object could be big (like byte-array of file). + * Do not use such strategy if object could be big (like Bitmap or long string). + * Do not use such strategy if object is mutable because multiple subscribers could then change it's state. + */ + CACHE_STORE_AND_ACTUAL_VALUE + + } + + /** + * Helper class to create various builders. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ + public static class BuilderCore { + + @NonNull + protected final TKey key; + @NonNull + protected final Type objectType; + @NonNull + private final Type storeObjectType; + @NonNull + private final Store store; + @NonNull + private final Converter converter; + @Nullable + private ObserveStrategy observeStrategy; + @Nullable + private Migration migration; + @Nullable + private TObject defaultValue; + @Nullable + private Scheduler storeScheduler; + private long cacheTimeMillis; + + protected BuilderCore(@NonNull final TKey key, + @NonNull final Type objectType, + @NonNull final Type storeObjectType, + @NonNull final Store store, + @NonNull final Converter converter) { + this(key, objectType, storeObjectType, store, converter, null, null, null, null, DEFAULT_CACHE_TIME_MILLIS); + } + + protected BuilderCore(@NonNull final BuilderCore sourceBuilder) { + this(sourceBuilder.key, sourceBuilder.objectType, sourceBuilder.storeObjectType, + sourceBuilder.store, sourceBuilder.converter, sourceBuilder.observeStrategy, + sourceBuilder.migration, sourceBuilder.defaultValue, sourceBuilder.storeScheduler, sourceBuilder.cacheTimeMillis); + } + + @SuppressWarnings({"PMD.ExcessiveParameterList", "CPD-START"}) + //CPD: it is same code as constructor of Storable + //ExcessiveParameterList: that's why we are using builder to create it + private BuilderCore(@NonNull final TKey key, + @NonNull final Type objectType, + @NonNull final Type storeObjectType, + @NonNull final Store store, + @NonNull final Converter converter, + @Nullable final ObserveStrategy observeStrategy, + @Nullable final Migration migration, + @Nullable final TObject defaultValue, + @Nullable final Scheduler storeScheduler, + final long cacheTimeMillis) { + this.key = key; + this.objectType = objectType; + this.storeObjectType = storeObjectType; + this.store = store; + this.converter = converter; + this.observeStrategy = observeStrategy; + this.migration = migration; + this.defaultValue = defaultValue; + this.storeScheduler = storeScheduler; + this.cacheTimeMillis = cacheTimeMillis; + } + + @SuppressWarnings("CPD-END") + protected void setStoreSchedulerInternal(@Nullable final Scheduler storeScheduler) { + this.storeScheduler = storeScheduler; + } + + protected void setObserveStrategyInternal(@Nullable final ObserveStrategy observeStrategy) { + this.observeStrategy = observeStrategy; + } + + protected void setMigrationInternal(@NonNull final Migration migration) { + this.migration = migration; + } + + protected void setCacheTimeInternal(final long cacheTime, @NonNull final TimeUnit timeUnit) { + this.cacheTimeMillis = timeUnit.toMillis(cacheTime); + } + + @Nullable + protected TObject getDefaultValue() { + return defaultValue; + } + + protected void setDefaultValueInternal(@NonNull final TObject defaultValue) { + this.defaultValue = defaultValue; + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Converter.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Converter.java new file mode 100644 index 0000000..5991932 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Converter.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Interface that is providing logic to convert value from specific type to type allowed to store in {@link Store} object and back. + * + * @param Type of original objects; + * @param Type of objects in store. + */ +public interface Converter { + + /** + * Converts specific object of objectType to object of storeObjectClass allowed to store. + * + * @param objectType Type of object; + * @param storeObjectType Type of store object allowed to store; + * @param object Object to be converted to store object; + * @return Object that is allowed to store into specific {@link Store}; + * @throws ConversionException Exception during conversion. Usually it indicates illegal state. + */ + @Nullable + TStoreObject toStoreObject(@NonNull Type objectType, @NonNull Type storeObjectType, @Nullable TObject object) + throws ConversionException; + + /** + * Converts specific store object of storeObjectClass to object of objectType. + * + * @param objectType Type of object; + * @param storeObjectType Type of store object allowed to store; + * @param storeObject Object from specific {@link Store}; + * @return Object converted from store object; + * @throws ConversionException Exception during conversion. Usually it indicates illegal state. + */ + @Nullable + TObject toObject(@NonNull Type objectType, @NonNull Type storeObjectType, @Nullable TStoreObject storeObject) + throws ConversionException; + + class ConversionException extends Exception { + + public ConversionException(@NonNull final String message) { + super(message); + } + + public ConversionException(@NonNull final String message, @NonNull final Throwable throwable) { + super(message, throwable); + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migration.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migration.java new file mode 100644 index 0000000..62397f2 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migration.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; + +import java.util.Arrays; +import java.util.List; + +import io.reactivex.Completable; +import io.reactivex.Flowable; +import io.reactivex.Single; + +/** + * Created by Gavriil Sitnikov on 06/10/2015. + * Object that allows to migrate some store objects from one version to another by migrators passed into constructor. + * Migrating objects should have same types of store key. + * + * @param Type of key of store objects. + */ +public class Migration { + + public static final long DEFAULT_VERSION = -1L; + + private final long latestVersion; + @NonNull + private final Store versionsStore; + @NonNull + private final List> migrators; + + @SafeVarargs + public Migration(@NonNull final Store versionsStore, + final long latestVersion, + @NonNull final Migrator... migrators) { + this.versionsStore = versionsStore; + this.latestVersion = latestVersion; + this.migrators = Arrays.asList(migrators); + } + + @NonNull + private Single loadCurrentVersion(@NonNull final TKey key) { + return versionsStore.loadObject(Long.class, key) + .map(version -> version.get() != null ? version.get() : DEFAULT_VERSION) + .onErrorResumeNext(throwable + -> Single.error(new MigrationException(String.format("Can't get version of '%s' from %s", key, versionsStore), throwable))); + } + + @NonNull + private Single makeMigrationChain(@NonNull final TKey key, @NonNull final VersionUpdater versionUpdater) { + Single chain = Single.fromCallable(() -> versionUpdater.initialVersion); + for (final Migrator migrator : migrators) { + chain = chain.flatMap(updatedVersion -> + migrator.canMigrate(key, updatedVersion) + .flatMap(canMigrate -> canMigrate + ? migrator.migrate(key, updatedVersion) + .doOnSuccess(newVersion + -> versionUpdater.updateVersion(newVersion, latestVersion, migrator)) + : Single.just(updatedVersion))); + } + return chain; + } + + /** + * Migrates some object by key to latest version. + * + * @param key Key of object to migrate. + */ + @NonNull + public Completable migrateToLatestVersion(@NonNull final TKey key) { + return loadCurrentVersion(key) + .flatMap(currentVersion -> { + final VersionUpdater versionUpdater = new VersionUpdater<>(key, versionsStore, currentVersion); + return makeMigrationChain(key, versionUpdater) + .doOnSuccess(lastUpdatedVersion -> { + if (lastUpdatedVersion < latestVersion) { + throw new NextLoopMigrationException(); + } + if (versionUpdater.initialVersion == versionUpdater.oldVersion) { + throw new MigrationException(String.format("Version of '%s' not updated from %s", + key, versionUpdater.initialVersion)); + } + }) + .retryWhen(attempts -> attempts + .switchMap(throwable -> throwable instanceof NextLoopMigrationException + ? Flowable.just(new Object()) : Flowable.error(throwable))); + }) + .ignoreElement() + .andThen(versionsStore.storeObject(Long.class, key, latestVersion)) + .onErrorResumeNext(throwable -> { + if (throwable instanceof MigrationException) { + return Completable.error(throwable); + } + return Completable.error(new MigrationException(String.format("Can't migrate '%s'", key), throwable)); + }); + } + + private static class VersionUpdater { + + @NonNull + private final TKey key; + @NonNull + private final Store versionsStore; + private long oldVersion; + private long initialVersion; + + public VersionUpdater(@NonNull final TKey key, @NonNull final Store versionsStore, final long initialVersion) { + this.key = key; + this.versionsStore = versionsStore; + this.oldVersion = initialVersion; + this.initialVersion = initialVersion; + } + + public void updateVersion(final long updateVersion, final long latestVersion, @NonNull final Migrator migrator) { + if (initialVersion > updateVersion) { + throw new MigrationException(String.format("Version of '%s' downgraded from %s to %s [from %s by %s]", + key, initialVersion, updateVersion, versionsStore, migrator)); + } + if (updateVersion > latestVersion) { + throw new MigrationException( + String.format("Version of '%s' is %s and higher than latest version %s [from %s by %s]", + key, initialVersion, updateVersion, versionsStore, migrator)); + } + if (updateVersion == initialVersion) { + throw new MigrationException(String.format("Update version of '%s' equals current version '%s' [from %s by %s]", + key, updateVersion, versionsStore, migrator)); + } + oldVersion = initialVersion; + initialVersion = updateVersion; + } + + } + + private static class NextLoopMigrationException extends Exception { + } + + public static class MigrationException extends RuntimeException { + + public MigrationException(@NonNull final String message) { + super(message); + } + + public MigrationException(@NonNull final String message, @NonNull final Throwable throwable) { + super(message, throwable); + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migrator.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migrator.java new file mode 100644 index 0000000..41a3c53 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Migrator.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; + +import io.reactivex.Single; + +/** + * Created by Gavriil Sitnikov on 05/10/2015. + * Abstract class of objects which are able to migrate some values from one version to another. + * Also it is able to move objects from one store to another. + * + * @param Type of keys of migrating values; + * @param Type of values from current store; + * @param Type of values from new store. Could be same as {@link TOldStoreObject}. + */ +public abstract class Migrator { + + @NonNull + private final Store oldStore; + @NonNull + private final Store newStore; + + public Migrator(@NonNull final Store oldStore, + @NonNull final Store newStore) { + this.oldStore = oldStore; + this.newStore = newStore; + } + + /** + * Returns if this migrator can migrate from specific version to some new version. + * + * @param version Version to migrate from; + * @return True if migrator supports migration from this version. + */ + public abstract boolean supportsMigrationFor(long version); + + /** + * Returns {@link Single} that emits if specific object with key of specific version could be migrated by this migrator. + * + * @param key Key of object to migrate; + * @param version Current version of object; + * @return {@link Single} that emits true if object with such key and version could be migrated. + */ + @NonNull + public Single canMigrate(@NonNull final TKey key, final long version) { + return supportsMigrationFor(version) ? oldStore.contains(key) : Single.just(false); + } + + /** + * Single that migrates object with specific key from some version to migrator's version. + * + * @param key Key of object to migrate; + * @param version Current version of object; + * @return {@link Single} that emits new version of object after migration process. + */ + @NonNull + public Single migrate(@NonNull final TKey key, final long version) { + return supportsMigrationFor(version) + ? migrateInternal(key, version, oldStore, newStore) + : Single.error(new Migration.MigrationException(String.format("Version %s of '%s' is not supported by %s", version, key, this))); + } + + /** + * Single that represents internal migration logic specified by implementation. + * + * @param key Key of object to migrate; + * @param version Current version of object; + * @param oldStore Old store of object; + * @param newStore new store of object; + * @return {@link Single} that emits new version of object after migration process. + */ + @NonNull + protected abstract Single migrateInternal(@NonNull TKey key, + long version, + @NonNull Store oldStore, + @NonNull Store newStore); + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/NonNullStorable.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/NonNullStorable.java new file mode 100644 index 0000000..3373ae6 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/NonNullStorable.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * {@link Storable} that should return not null value on get. + * If this rule is violated then it will throw {@link ShouldNotHappenException}. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ +public class NonNullStorable extends BaseStorable { + + public NonNullStorable(@NonNull final Builder builderCore) { + super(builderCore); + } + + @NonNull + @Override + public Observable observe() { + return observeOptionalValue() + .map(optional -> { + if (optional.get() == null) { + throw new ShouldNotHappenException(); + } + return optional.get(); + }); + } + + /** + * Created by Gavriil Sitnikov on 15/05/2016. + * Builder that is already contains not null default value. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ + @SuppressWarnings("CPD-START") + //CPD: it is same code as Builder of Storable because it's methods returning this and can't be inherited + public static class Builder extends BuilderCore { + + public Builder(@NonNull final Storable.Builder sourceBuilder, + @NonNull final TObject defaultValue) { + super(sourceBuilder); + if (defaultValue == null) { + throw new ShouldNotHappenException(); + } + setDefaultValueInternal(defaultValue); + } + + /** + * Sets specific {@link Scheduler} to store/load/convert values on it. + * + * @param storeScheduler Scheduler; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setStoreScheduler(@Nullable final Scheduler storeScheduler) { + setStoreSchedulerInternal(storeScheduler); + return this; + } + + /** + * Sets specific {@link ObserveStrategy} to cache value in memory in specific way. + * + * @param observeStrategy ObserveStrategy; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setObserveStrategy(@Nullable final ObserveStrategy observeStrategy) { + setObserveStrategyInternal(observeStrategy); + return this; + } + + /** + * Sets cache time for while value that cached by {@link #setObserveStrategy(ObserveStrategy)} + * will be in memory after everyone unsubscribe. + * It is important for example for cases when user switches between screens and hide/open app very fast. + * + * @param cacheTime Cache time value; + * @param timeUnit Cache time units. + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setCacheTime(final long cacheTime, @NonNull final TimeUnit timeUnit) { + setCacheTimeInternal(cacheTime, timeUnit); + return this; + } + + /** + * Sets specific {@link Migration} to migrate values from specific version to latest version. + * + * @param migration Migration; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setMigration(@NonNull final Migration migration) { + setMigrationInternal(migration); + return this; + } + + /** + * Building {@link NonNullStorable} object. + * + * @return New {@link NonNullStorable}. + */ + @NonNull + @SuppressWarnings("CPD-END") + public NonNullStorable build() { + if (getDefaultValue() == null) { + throw new ShouldNotHappenException(); + } + return new NonNullStorable<>(this); + } + + } +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/SameTypesConverter.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/SameTypesConverter.java new file mode 100644 index 0000000..9bfa228 --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/SameTypesConverter.java @@ -0,0 +1,27 @@ +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; + +/** + * Simple safe converter that is doing nothing on conversion. + * + * @param Same type. + */ +public class SameTypesConverter implements Converter { + + @Nullable + @Override + public T toStoreObject(@NonNull final Type type1, @NonNull final Type type2, @Nullable final T object) { + return object; + } + + @Nullable + @Override + public T toObject(@NonNull final Type type1, @NonNull final Type type2, @Nullable final T object) { + return object; + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Storable.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Storable.java new file mode 100644 index 0000000..b891c0c --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Storable.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; +import java.util.concurrent.TimeUnit; + +import io.reactivex.Observable; +import io.reactivex.Scheduler; +import ru.touchin.roboswag.core.utils.Optional; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Base class allows to async access to some store. + * Supports conversion between store and actual value. If it is not needed then use {@link SameTypesConverter} + * Supports migration from specific version to latest by {@link Migration} object. + * Allows to set default value which will be returned if actual value is null. + * Allows to declare specific {@link ObserveStrategy}. + * Also specific {@link Scheduler} could be specified to not create new scheduler per storable. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ +public class Storable extends BaseStorable> { + + public Storable(@NonNull final BuilderCore builderCore) { + super(builderCore); + } + + @NonNull + @Override + public Observable> observe() { + return observeOptionalValue(); + } + + /** + * Helper class to build {@link Storable}. + * + * @param Type of key to identify object; + * @param Type of actual object; + * @param Type of store object. Could be same as {@link TObject}. + */ + public static class Builder extends BuilderCore { + + public Builder(@NonNull final TKey key, + @NonNull final Type objectType, + @NonNull final Type storeObjectType, + @NonNull final Store store, + @NonNull final Converter converter) { + super(key, objectType, storeObjectType, store, converter); + } + + /** + * Sets specific {@link Scheduler} to store/load/convert values on it. + * + * @param storeScheduler Scheduler; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setStoreScheduler(@Nullable final Scheduler storeScheduler) { + setStoreSchedulerInternal(storeScheduler); + return this; + } + + /** + * Sets specific {@link ObserveStrategy} to cache value in memory in specific way. + * + * @param observeStrategy ObserveStrategy; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setObserveStrategy(@Nullable final ObserveStrategy observeStrategy) { + setObserveStrategyInternal(observeStrategy); + return this; + } + + /** + * Sets cache time for while value that cached by {@link #setObserveStrategy(ObserveStrategy)} will be in memory after everyone unsubscribe. + * It is important for example for cases when user switches between screens and hide/open app very fast. + * + * @param cacheTime Cache time value; + * @param timeUnit Cache time units. + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setCacheTime(final long cacheTime, @NonNull final TimeUnit timeUnit) { + setCacheTimeInternal(cacheTime, timeUnit); + return this; + } + + /** + * Sets specific {@link Migration} to migrate values from specific version to latest version. + * + * @param migration Migration; + * @return Builder that allows to specify other fields. + */ + @NonNull + public Builder setMigration(@NonNull final Migration migration) { + setMigrationInternal(migration); + return this; + } + + /** + * Sets value which will be returned instead of null. + * + * @param defaultValue Default value; + * @return Builder that allows to specify other fields. + */ + @NonNull + public NonNullStorable.Builder setDefaultValue(@NonNull final TObject defaultValue) { + return new NonNullStorable.Builder<>(this, defaultValue); + } + + /** + * Building {@link Storable} object. + * + * @return New {@link Storable}. + */ + @NonNull + public Storable build() { + return new Storable<>(this); + } + + } + +} diff --git a/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Store.java b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Store.java new file mode 100644 index 0000000..ad31c4d --- /dev/null +++ b/storable/src/main/java/ru/touchin/roboswag/core/observables/storable/Store.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.observables.storable; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.lang.reflect.Type; + +import io.reactivex.Completable; +import io.reactivex.Single; +import ru.touchin.roboswag.core.utils.Optional; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Interface that is providing access to abstract object which can store (e.g. in file, database, remote store) + * some type of objects (e.g. String, byte[], Integer) by key. + * + * @param Type of keys for values; + * @param Type of values stored in store. + */ +public interface Store { + + /** + * Returns if store contains specific key related to some value. + * + * @param key Key which is finding in store; + * @return True if key have found in store. + */ + @NonNull + Single contains(@NonNull TKey key); + + /** + * Stores object to store with related key. + * + * @param storeObjectType Type of object to store; + * @param key Key related to object; + * @param storeObject Object to store; + */ + @NonNull + Completable storeObject(@NonNull Type storeObjectType, @NonNull TKey key, @Nullable TStoreObject storeObject); + + /** + * Loads object from store by key. + * + * @param storeObjectType Type of object to store; + * @param key Key related to object; + * @return Object from store found by key; + */ + @NonNull + Single> loadObject(@NonNull Type storeObjectType, @NonNull TKey key); + + /** + * Stores object to store with related key. + * + * @param storeObjectType Type of object to store; + * @param key Key related to object; + * @param storeObject Object to store; + */ + void setObject(@NonNull Type storeObjectType, @NonNull TKey key, @Nullable TStoreObject storeObject); + + /** + * Gets object from store by key. + * + * @param storeObjectType Type of object to store; + * @param key Key related to object; + * @return Object from store found by key; + */ + @Nullable + TStoreObject getObject(@NonNull Type storeObjectType, @NonNull TKey key); + +} diff --git a/templates/.gitignore b/templates/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/templates/.gitignore @@ -0,0 +1 @@ +/build diff --git a/templates/build.gradle b/templates/build.gradle new file mode 100644 index 0000000..1ee1e3b --- /dev/null +++ b/templates/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":utils") + api project(":logging") + api project(":api-logansquare") + api project(":navigation") + + api 'com.android.support:multidex:1.0.3' + + api 'net.danlew:android.joda:2.9.9.4' + + implementation "com.android.support:appcompat-v7:$versions.supportLibrary" + + implementation("com.crashlytics.sdk.android:crashlytics:$versions.crashlytics@aar") { + transitive = true + } +} diff --git a/templates/src/main/AndroidManifest.xml b/templates/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ad64e1e --- /dev/null +++ b/templates/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + diff --git a/templates/src/main/java/ru/touchin/templates/TouchinActivity.java b/templates/src/main/java/ru/touchin/templates/TouchinActivity.java new file mode 100644 index 0000000..41b2e28 --- /dev/null +++ b/templates/src/main/java/ru/touchin/templates/TouchinActivity.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates; + +import android.app.ActivityManager; +import android.content.Intent; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; + +import ru.touchin.roboswag.components.navigation.activities.BaseActivity; +import ru.touchin.roboswag.core.log.Lc; + +/** + * Created by Gavriil Sitnikov on 11/03/16. + * Base class of activity to extends for Touch Instinct related projects. + */ +public abstract class TouchinActivity extends BaseActivity { + + @Override + protected void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Possible work around for market launches. See http://code.google.com/p/android/issues/detail?id=2373 + // for more details. Essentially, the market launches the main activity on top of other activities. + // we never want this to happen. Instead, we check if we are the root and if not, we finish. + if (!isTaskRoot() && getIntent().hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(getIntent().getAction())) { + Lc.e("Finishing activity as it is launcher but not root"); + finish(); + } + } + + /** + * Setup task description of application for Android 5.0 and later. It is showing when user opens task bar. + * + * @param label Name of application to show in task bar; + * @param iconRes Icon of application to show in task bar; + * @param primaryColorRes Color of application to show in task bar. + */ + protected void setupTaskDescriptor(@NonNull final String label, @DrawableRes final int iconRes, @ColorRes final int primaryColorRes) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final ActivityManager.TaskDescription taskDescription = new ActivityManager.TaskDescription(label, + ((BitmapDrawable) ContextCompat.getDrawable(this, iconRes)).getBitmap(), + ContextCompat.getColor(this, primaryColorRes)); + setTaskDescription(taskDescription); + } + } + +} diff --git a/templates/src/main/java/ru/touchin/templates/TouchinApp.java b/templates/src/main/java/ru/touchin/templates/TouchinApp.java new file mode 100644 index 0000000..8d33cb0 --- /dev/null +++ b/templates/src/main/java/ru/touchin/templates/TouchinApp.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates; + +import android.app.Application; +import android.content.Context; +import android.os.StrictMode; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.multidex.MultiDex; +import android.util.Log; + +import com.crashlytics.android.Crashlytics; + +import net.danlew.android.joda.JodaTimeAndroid; + +import java.util.ArrayList; +import java.util.List; + +import io.fabric.sdk.android.Fabric; +import ru.touchin.roboswag.core.log.ConsoleLogProcessor; +import ru.touchin.roboswag.core.log.Lc; +import ru.touchin.roboswag.core.log.LcGroup; +import ru.touchin.roboswag.core.log.LcLevel; +import ru.touchin.roboswag.core.log.LogProcessor; +import ru.touchin.roboswag.core.utils.ShouldNotHappenException; + +/** + * Created by Gavriil Sitnikov on 10/03/16. + * Base class of application to extends for Touch Instinct related projects. + */ +public abstract class TouchinApp extends Application { + + @Override + protected void attachBaseContext(@NonNull final Context base) { + super.attachBaseContext(base); + MultiDex.install(base); + } + + @Override + public void onCreate() { + super.onCreate(); + JodaTimeAndroid.init(this); + if (BuildConfig.DEBUG) { + enableStrictMode(); + Lc.initialize(new ConsoleLogProcessor(LcLevel.VERBOSE), true); + LcGroup.UI_LIFECYCLE.disable(); + } else { + try { + final Crashlytics crashlytics = new Crashlytics(); + Fabric.with(this, crashlytics); + Fabric.getLogger().setLogLevel(Log.ERROR); + Lc.initialize(new CrashlyticsLogProcessor(crashlytics), false); + } catch (final NoClassDefFoundError error) { + Lc.initialize(new ConsoleLogProcessor(LcLevel.INFO), false); + Lc.e("Crashlytics initialization error! Did you forget to add\n" + + "compile('com.crashlytics.sdk.android:crashlytics:+@aar') {\n" + + " transitive = true;\n" + + "}\n" + + "to your build.gradle?", error); + } + } + } + + private void enableStrictMode() { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .permitDiskReads() + .permitDiskWrites() + .penaltyLog() + .build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog() + .build()); + } + + private static class CrashlyticsLogProcessor extends LogProcessor { + + @NonNull + private final Crashlytics crashlytics; + + public CrashlyticsLogProcessor(@NonNull final Crashlytics crashlytics) { + super(LcLevel.INFO); + this.crashlytics = crashlytics; + } + + @Override + public void processLogMessage(@NonNull final LcGroup group, + @NonNull final LcLevel level, + @NonNull final String tag, + @NonNull final String message, + @Nullable final Throwable throwable) { + if (group == LcGroup.UI_LIFECYCLE) { + crashlytics.core.log(level.getPriority(), tag, message); + } else if (!level.lessThan(LcLevel.ASSERT) + || (group == ApiModel.API_VALIDATION_LC_GROUP && level == LcLevel.ERROR)) { + Log.e(tag, message); + if (throwable != null) { + crashlytics.core.log(level.getPriority(), tag, message); + crashlytics.core.logException(throwable); + } else { + final ShouldNotHappenException exceptionToLog = new ShouldNotHappenException(tag + ':' + message); + reduceStackTrace(exceptionToLog); + crashlytics.core.logException(exceptionToLog); + } + } + } + + private void reduceStackTrace(@NonNull final Throwable throwable) { + final StackTraceElement[] stackTrace = throwable.getStackTrace(); + final List reducedStackTraceList = new ArrayList<>(); + for (int i = stackTrace.length - 1; i >= 0; i--) { + final StackTraceElement stackTraceElement = stackTrace[i]; + if (stackTraceElement.getClassName().contains(getClass().getSimpleName()) + || stackTraceElement.getClassName().contains(LcGroup.class.getName()) + || stackTraceElement.getClassName().contains(Lc.class.getName())) { + break; + } + reducedStackTraceList.add(0, stackTraceElement); + } + StackTraceElement[] reducedStackTrace = new StackTraceElement[reducedStackTraceList.size()]; + reducedStackTrace = reducedStackTraceList.toArray(reducedStackTrace); + throwable.setStackTrace(reducedStackTrace); + } + + } + +} diff --git a/utils/.gitignore b/utils/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/utils/.gitignore @@ -0,0 +1 @@ +/build diff --git a/utils/build.gradle b/utils/build.gradle new file mode 100644 index 0000000..69333c1 --- /dev/null +++ b/utils/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation "com.android.support:support-annotations:$versions.supportLibrary" +} diff --git a/utils/src/main/AndroidManifest.xml b/utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6d76123 --- /dev/null +++ b/utils/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/utils/src/main/java/ru/touchin/defaults/DefaultTextWatcher.java b/utils/src/main/java/ru/touchin/defaults/DefaultTextWatcher.java new file mode 100644 index 0000000..563c518 --- /dev/null +++ b/utils/src/main/java/ru/touchin/defaults/DefaultTextWatcher.java @@ -0,0 +1,24 @@ +package ru.touchin.defaults; + +import android.support.annotation.NonNull; +import android.text.Editable; +import android.text.TextWatcher; + +public class DefaultTextWatcher implements 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) { + // Do nothing + } + + @Override + public void afterTextChanged(@NonNull final Editable editable) { + // Do nothing + } + +} diff --git a/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java b/utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java similarity index 65% rename from src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java rename to utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java index 8a19035..a80ee72 100644 --- a/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/UiUtils.java @@ -25,12 +25,8 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.Build; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.IdRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Display; @@ -40,12 +36,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; - -import java.util.concurrent.atomic.AtomicInteger; - -import ru.touchin.roboswag.components.navigation.activities.BaseActivity; -import ru.touchin.roboswag.core.log.LcGroup; -import rx.functions.Action0; +import android.view.inputmethod.InputMethodManager; /** * Created by Gavriil Sitnikov on 13/11/2015. @@ -53,21 +44,6 @@ import rx.functions.Action0; */ public final class UiUtils { - /** - * Logging group to log UI metrics (like inflation or layout time etc.). - */ - public static final LcGroup UI_METRICS_LC_GROUP = new LcGroup("UI_METRICS"); - /** - * Logging group to log UI lifecycle (onCreate, onStart, onResume etc.). - */ - public static final LcGroup UI_LIFECYCLE_LC_GROUP = new LcGroup("UI_LIFECYCLE"); - /** - * Delay to let user view ripple effect before screen changed. - */ - public static final long RIPPLE_EFFECT_DELAY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 150 : 0; - - private static final Handler RIPPLE_HANDLER = new Handler(Looper.getMainLooper()); - /** * Method to inflate view with right layout parameters based on container and add inflated view as a child to it. * @@ -93,67 +69,6 @@ public final class UiUtils { return LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false); } - /** - * Sets click listener to view. On click it will call something after delay. - * - * @param targetView View to set click listener to; - * @param onClickListener Click listener; - * @param delay Delay after which click listener will be called. - */ - public static void setOnRippleClickListener(@NonNull final View targetView, @Nullable final Action0 onClickListener, final long delay) { - setOnRippleClickListener(targetView, onClickListener != null ? v -> onClickListener.call() : null, delay); - } - - /** - * Sets click listener to view. On click it will call something with {@link #RIPPLE_EFFECT_DELAY}. - * - * @param targetView View to set click listener to; - * @param onClickListener Click listener. - */ - public static void setOnRippleClickListener(@NonNull final View targetView, @Nullable final Action0 onClickListener) { - setOnRippleClickListener(targetView, onClickListener != null ? v -> onClickListener.call() : null, RIPPLE_EFFECT_DELAY); - } - - /** - * Sets click listener to view. On click it will call something with {@link #RIPPLE_EFFECT_DELAY}. - * - * @param targetView View to set click listener to; - * @param onClickListener Click listener. - */ - public static void setOnRippleClickListener(@NonNull final View targetView, @Nullable final View.OnClickListener onClickListener) { - setOnRippleClickListener(targetView, onClickListener, RIPPLE_EFFECT_DELAY); - } - - /** - * Sets click listener to view. On click it will call something after delay. - * - * @param targetView View to set click listener to; - * @param onClickListener Click listener; - * @param delay Delay after which click listener will be called. - */ - public static void setOnRippleClickListener(@NonNull final View targetView, - @Nullable final View.OnClickListener onClickListener, - final long delay) { - if (onClickListener == null) { - targetView.setOnClickListener(null); - return; - } - - final Runnable runnable = () -> { - if (targetView.getWindowVisibility() != View.VISIBLE - || !targetView.hasWindowFocus() - || (targetView.getContext() instanceof BaseActivity && !((BaseActivity) targetView.getContext()).isActuallyResumed())) { - return; - } - onClickListener.onClick(targetView); - }; - - targetView.setOnClickListener(v -> { - RIPPLE_HANDLER.removeCallbacksAndMessages(null); - RIPPLE_HANDLER.postDelayed(runnable, delay); - }); - } - private UiUtils() { } @@ -298,35 +213,6 @@ public final class UiUtils { */ public static class OfViews { - private static final int GENERATED_ID_THRESHOLD = 0x00FFFFFF; - private static final AtomicInteger NEXT_GENERATED_ID = new AtomicInteger(1); - - /** - * Generates unique ID for view. See android {@link View#generateViewId()}. - * - * @return Unique ID. - */ - @IdRes - public static int generateViewId() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - return View.generateViewId(); - } - int result = 0; - boolean isGenerated = false; - while (!isGenerated) { - result = NEXT_GENERATED_ID.get(); - // aapt-generated IDs have the high byte nonzero; clamp to the range under that. - int newValue = result + 1; - if (newValue > GENERATED_ID_THRESHOLD) { - newValue = 1; // Roll over to 1, not 0. - } - if (NEXT_GENERATED_ID.compareAndSet(result, newValue)) { - isGenerated = true; - } - } - return result; - } - /** * Returns string representation of {@link View}'s ID. * @@ -342,6 +228,42 @@ public final class UiUtils { } } + /** + * Hides device keyboard for target activity. + */ + public static void hideSoftInput(@NonNull final Activity activity) { + final View focusedView = activity.getCurrentFocus(); + if (focusedView != null) { + hideSoftInput(focusedView); + } + } + + /** + * Hides device keyboard for target view. + */ + public static void hideSoftInput(@NonNull final View view) { + view.clearFocus(); + final InputMethodManager inputManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputManager != null) { + inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + /** + * Shows device keyboard over {@link Activity} and focuses {@link View}. + * Do NOT use it if keyboard is over {@link android.app.Dialog} - it won't work as they have different {@link Activity#getWindow()}. + * Do NOT use it if you are not sure that view is already added on screen. + * + * @param view View to get focus for input from keyboard. + */ + public static void showSoftInput(@NonNull final View view) { + view.requestFocus(); + final InputMethodManager inputManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputManager != null) { + inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + } + } + private OfViews() { } diff --git a/src/main/java/ru/touchin/roboswag/components/utils/spans/ColoredUrlSpan.java b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/ColoredUrlSpan.java similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/utils/spans/ColoredUrlSpan.java rename to utils/src/main/java/ru/touchin/roboswag/components/utils/spans/ColoredUrlSpan.java diff --git a/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java similarity index 93% rename from src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java rename to utils/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java index 5f84f91..b350912 100644 --- a/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java +++ b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/PhoneSpan.java @@ -8,8 +8,6 @@ import android.text.TextPaint; import android.text.style.URLSpan; import android.view.View; -import ru.touchin.roboswag.core.log.Lc; - /** * Created by Gavriil Sitnikov on 14/11/2015. * Span that is opening phone call intent. @@ -28,7 +26,7 @@ public class PhoneSpan extends URLSpan { intent.setData(Uri.parse(getURL())); widget.getContext().startActivity(intent); } catch (final ActivityNotFoundException exception) { - Lc.assertion(exception); + // Do nothing } } diff --git a/src/main/java/ru/touchin/roboswag/components/utils/spans/TypefaceSpan.java b/utils/src/main/java/ru/touchin/roboswag/components/utils/spans/TypefaceSpan.java similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/utils/spans/TypefaceSpan.java rename to utils/src/main/java/ru/touchin/roboswag/components/utils/spans/TypefaceSpan.java diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/ObjectUtils.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/ObjectUtils.java new file mode 100644 index 0000000..a8c2baf --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/ObjectUtils.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + * Created by Gavriil Sitnikov on 04/10/2015. + * Some utilities related to objects. + */ +public final class ObjectUtils { + + /** + * Compares two objects if they are equals or not. If they are arrays then compare process same as {@link Arrays#deepEquals(Object[], Object[])}. + * + * @param object1 First object to compare; + * @param object2 Second object to compare; + * @return True if objects are equals. + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + //CompareObjectsWithEquals: we need to compare if it's same object + public static boolean equals(@Nullable final Object object1, @Nullable final Object object2) { + if (object1 == object2) { + return true; + } + if (object1 == null || object2 == null) { + return false; + } + + final Class elementType1 = object1.getClass().getComponentType(); + final Class elementType2 = object2.getClass().getComponentType(); + + if (!(elementType1 == null ? elementType2 == null : elementType1.equals(elementType2))) { + return false; + } + if (elementType1 == null) { + return object1.equals(object2); + } + return isArraysEquals(object1, object2, elementType1); + } + + /** + * Compares two collections if their elements are equals or not. + * + * @param collection1 First object to compare; + * @param collection2 Second object to compare; + * @return True if collections are equals. + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + //CompareObjectsWithEquals: we need to compare if it's same object + public static boolean isCollectionsEquals(@Nullable final Collection collection1, @Nullable final Collection collection2) { + if (collection1 == collection2) { + return true; + } + if (collection1 == null || collection2 == null) { + return false; + } + if (collection1.size() != collection2.size()) { + return false; + } + final Iterator collection2Iterator = collection2.iterator(); + for (final Object item1 : collection1) { + if (!equals(item1, collection2Iterator.next())) { + return false; + } + } + return true; + } + + /** + * Compares two maps if their elements are equals or not. + * + * @param map1 First object to compare; + * @param map2 Second object to compare; + * @return True if maps are equals. + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + //CompareObjectsWithEquals: we need to compare if it's same object + public static boolean isMapsEquals(@Nullable final Map map1, @Nullable final Map map2) { + return map1 == map2 || !(map1 == null || map2 == null) + && map1.size() == map2.size() + && map1.entrySet().containsAll(map2.entrySet()) + && map2.entrySet().containsAll(map1.entrySet()); + } + + @SuppressWarnings("PMD.AvoidUsingShortType") + private static boolean isArraysEquals(@NonNull final Object object1, @Nullable final Object object2, @NonNull final Class elementType) { + if (object1 instanceof Object[]) { + return Arrays.deepEquals((Object[]) object1, (Object[]) object2); + } else if (elementType == int.class) { + return Arrays.equals((int[]) object1, (int[]) object2); + } else if (elementType == char.class) { + return Arrays.equals((char[]) object1, (char[]) object2); + } else if (elementType == boolean.class) { + return Arrays.equals((boolean[]) object1, (boolean[]) object2); + } else if (elementType == byte.class) { + return Arrays.equals((byte[]) object1, (byte[]) object2); + } else if (elementType == long.class) { + return Arrays.equals((long[]) object1, (long[]) object2); + } else if (elementType == float.class) { + return Arrays.equals((float[]) object1, (float[]) object2); + } else if (elementType == double.class) { + return Arrays.equals((double[]) object1, (double[]) object2); + } else { + return Arrays.equals((short[]) object1, (short[]) object2); + } + } + + /** + * Calculates hashCode() of several objects. + * + * @param objects Objects to combine hashCode() of; + * @return Calculated hashCode(). + */ + public static int hashCode(@Nullable final Object... objects) { + return Arrays.hashCode(objects); + } + + /** + * Returns if class is simple like primitive, enum or string. + * + * @param objectClass Class to check if it's simple class; + * @return True if class is simple. + */ + public static boolean isSimpleClass(@NonNull final Class objectClass) { + return objectClass.isPrimitive() || objectClass.getSuperclass() == Number.class + || objectClass.isEnum() || objectClass == Boolean.class + || objectClass == String.class || objectClass == Object.class; + } + + /** + * Returns true if collection is null or empty. + * + * @param collection Collection to check; + * @return True if collection is null or empty. + */ + public static boolean isNullOrEmpty(@Nullable final Collection collection) { + return collection == null || collection.isEmpty(); + } + + /** + * Returns true if map is null or empty. + * + * @param map Map to check; + * @return True if map is null or empty. + */ + public static boolean isNullOrEmpty(@Nullable final Map map) { + return map == null || map.isEmpty(); + } + + private ObjectUtils() { + } + +} diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/Optional.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/Optional.java new file mode 100644 index 0000000..86a8493 --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/Optional.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.Nullable; + +import java.io.Serializable; + +/** + * Created by Gavriil Sitnikov on 16/04/2017. + * Holds nullable objects inside. It is needed to implement RxJava2 non-null emitting logic. + * + * @param Type of object. + */ +public class Optional implements Serializable { + + private static final long serialVersionUID = 1L; + + @Nullable + private final T value; + + public Optional(@Nullable final T value) { + this.value = value; + } + + /** + * Returns holding nullable object. + * + * @return Holding object. + */ + @Nullable + public T get() { + return value; + } + + @Override + public boolean equals(@Nullable final Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + final Optional that = (Optional) object; + return ObjectUtils.equals(value, that.value); + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + +} diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/ServiceBinder.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/ServiceBinder.java new file mode 100644 index 0000000..82f8acf --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/ServiceBinder.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.app.Service; +import android.os.Binder; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * Created by Gavriil Sitnikov on 03/10/2015. + * Basic binding to {@link Service} which holds service object inside. + */ +public class ServiceBinder extends Binder { + + @NonNull + private final TService service; + + public ServiceBinder(@NonNull final TService service) { + super(); + this.service = service; + } + + /** + * Returns service which created this binder. + * + * @return Returns service. + */ + @NonNull + public TService getService() { + return service; + } + + @Override + public boolean equals(@Nullable final Object object) { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + + final ServiceBinder that = (ServiceBinder) object; + + return ObjectUtils.equals(service, that.service); + } + + @Override + public int hashCode() { + return service.hashCode(); + } + +} diff --git a/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java b/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java new file mode 100644 index 0000000..639a5ab --- /dev/null +++ b/utils/src/main/java/ru/touchin/roboswag/core/utils/StringUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov) + * + * This file is part of RoboSwag library. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package ru.touchin.roboswag.core.utils; + +import android.support.annotation.NonNull; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Created by Gavriil Sitnikov on 29/08/2016. + * Utility class to providing some string-related helper methods. + */ +public final class StringUtils { + + /** + * Returns MD5 of string. + * + * @param string String to get MD5 from; + * @return MD5 of string. + */ + @NonNull + public static String md5(@NonNull final String string) throws NoSuchAlgorithmException, UnsupportedEncodingException { + final MessageDigest digest = MessageDigest.getInstance("MD5"); + digest.update(string.getBytes("UTF-8")); + final byte[] messageDigestArray = digest.digest(); + + final StringBuilder hexString = new StringBuilder(); + for (final byte messageDigest : messageDigestArray) { + final String hex = Integer.toHexString(0xFF & messageDigest); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } + + private StringUtils() { + } + +} diff --git a/utils/src/main/java/ru/touchin/templates/DeviceUtils.java b/utils/src/main/java/ru/touchin/templates/DeviceUtils.java new file mode 100644 index 0000000..8a7e1fb --- /dev/null +++ b/utils/src/main/java/ru/touchin/templates/DeviceUtils.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Process; +import android.support.annotation.NonNull; +import android.telephony.TelephonyManager; + +/** + * Utility class that is providing common methods related to android device. + */ +public final class DeviceUtils { + + /** + * Detects active network type. + * + * @param context Application context + * @return Active network type {@link NetworkType} + */ + @NonNull + public static NetworkType getNetworkType(@NonNull final Context context) { + if (context.checkPermission(Manifest.permission.ACCESS_NETWORK_STATE, Process.myPid(), Process.myUid()) + != PackageManager.PERMISSION_GRANTED) { + return NetworkType.UNKNOWN; + } + final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) { + return NetworkType.UNKNOWN; + } + @SuppressLint("MissingPermission") final NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null || !info.isConnected()) { + return NetworkType.NONE; + } + switch (info.getType()) { + case ConnectivityManager.TYPE_WIFI: + return NetworkType.WI_FI; + case ConnectivityManager.TYPE_MOBILE: + return getMobileNetworkType(info); + default: + return NetworkType.UNKNOWN; + } + } + + /** + * Detects if some network connected. + * + * @param context Application context + * @return true if network connected, false otherwise. + */ + public static boolean isNetworkConnected(@NonNull final Context context) { + return getNetworkType(context) != NetworkType.NONE; + } + + @NonNull + private static NetworkType getMobileNetworkType(@NonNull final NetworkInfo info) { + switch (info.getSubtype()) { + case TelephonyManager.NETWORK_TYPE_GPRS: + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_1xRTT: + case TelephonyManager.NETWORK_TYPE_IDEN: + return NetworkType.MOBILE_2G; + case TelephonyManager.NETWORK_TYPE_UMTS: + case TelephonyManager.NETWORK_TYPE_EVDO_0: + case TelephonyManager.NETWORK_TYPE_EVDO_A: + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_EVDO_B: + case TelephonyManager.NETWORK_TYPE_EHRPD: + case TelephonyManager.NETWORK_TYPE_HSPAP: + return NetworkType.MOBILE_3G; + case TelephonyManager.NETWORK_TYPE_LTE: + case 19: // NETWORK_TYPE_LTE_CA is hide + return NetworkType.MOBILE_LTE; + case TelephonyManager.NETWORK_TYPE_UNKNOWN: + default: + return NetworkType.UNKNOWN; + } + } + + private DeviceUtils() { + } + + /** + * Available network types. + */ + public enum NetworkType { + /** + * Mobile 2G network. + */ + MOBILE_2G("2g"), + /** + * Mobile 3G network. + */ + MOBILE_3G("3g"), + /** + * Mobile LTE network. + */ + MOBILE_LTE("lte"), + /** + * Wi-Fi network. + */ + WI_FI("Wi-Fi"), + /** + * Unknown network type. + */ + UNKNOWN("unknown"), + /** + * No network. + */ + NONE("none"); + + @NonNull + private final String name; + + NetworkType(@NonNull final String name) { + this.name = name; + } + + /** + * @return Network type readable name. + */ + @NonNull + public String getName() { + return name; + } + + } + +} diff --git a/src/main/res/values/common_resources.xml b/utils/src/main/res/values/common_resources.xml similarity index 96% rename from src/main/res/values/common_resources.xml rename to utils/src/main/res/values/common_resources.xml index fff1084..f0221c8 100644 --- a/src/main/res/values/common_resources.xml +++ b/utils/src/main/res/values/common_resources.xml @@ -1,5 +1,5 @@ - + #0D000000 @@ -43,4 +43,4 @@ #E6FFFFFF #F3FFFFFF - \ No newline at end of file + diff --git a/views/.gitignore b/views/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/views/.gitignore @@ -0,0 +1 @@ +/build diff --git a/views/build.gradle b/views/build.gradle new file mode 100644 index 0000000..9ea4f9e --- /dev/null +++ b/views/build.gradle @@ -0,0 +1,21 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion versions.compileSdk + + defaultConfig { + minSdkVersion versions.minSdk + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api project(":utils") + api project(":logging") + + implementation "com.android.support:design:$versions.supportLibrary" +} diff --git a/views/src/main/AndroidManifest.xml b/views/src/main/AndroidManifest.xml new file mode 100644 index 0000000..354617c --- /dev/null +++ b/views/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java b/views/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java similarity index 97% rename from src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java rename to views/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java index 8e61895..c3b88ef 100644 --- a/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java +++ b/views/src/main/java/ru/touchin/roboswag/components/views/MaterialLoadingBar.java @@ -29,7 +29,6 @@ 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; /** @@ -90,7 +89,7 @@ public class MaterialLoadingBar extends AppCompatImageView { typedArray.recycle(); progressDrawable = new MaterialProgressDrawable(context, size); - progressDrawable.setColor(color); + setColor(color); progressDrawable.setStrokeWidth(strokeWidth); setScaleType(ScaleType.CENTER); setImageDrawable(progressDrawable); @@ -107,7 +106,7 @@ public class MaterialLoadingBar extends AppCompatImageView { progressDrawable.stop(); super.onDetachedFromWindow(); } - + /** * Set color of loader. * @@ -117,4 +116,4 @@ public class MaterialLoadingBar extends AppCompatImageView { progressDrawable.setColor(colorInt); } -} \ No newline at end of file +} diff --git a/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java b/views/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java similarity index 100% rename from src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java rename to views/src/main/java/ru/touchin/roboswag/components/views/MaterialProgressDrawable.java diff --git a/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java b/views/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java similarity index 85% rename from src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java rename to views/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java index 33c8493..40415e5 100644 --- a/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java +++ b/views/src/main/java/ru/touchin/roboswag/components/views/TypefacedEditText.java @@ -23,46 +23,36 @@ 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.utils.Typefaces; +import ru.touchin.defaults.DefaultTextWatcher; 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") +@Deprecated //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; @@ -96,23 +86,32 @@ public class TypefacedEditText extends AppCompatEditText { } else { setSingleLine(); } - - if (!isInEditMode()) { - setTypeface(Typefaces.getFromAttributes(context, attrs, R.styleable.TypefacedEditText, R.styleable.TypefacedEditText_customTypeface)); - } typedArray.recycle(); - if (inDebugMode) { + if (BuildConfig.DEBUG) { checkAttributes(context, attrs); } } } + @Nullable + public InputConnection onCreateInputConnection(@NonNull final EditorInfo attrs) { + final InputConnection inputConnection = super.onCreateInputConnection(attrs); + if (inputConnection != null && attrs.hintText == null) { + for (ViewParent parent = getParent(); parent instanceof View; parent = parent.getParent()) { + if (parent instanceof TextInputLayout) { + attrs.hintText = ((TextInputLayout) parent).getHint(); + break; + } + } + } + + return inputConnection; + } + private void checkAttributes(@NonNull final Context context, @NonNull final AttributeSet attrs) { final List errors = new ArrayList<>(); Boolean multiline = null; TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedEditText); - AttributesUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedEditText_customTypeface, true, - "customTypeface required parameter"); AttributesUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedEditText_isMultiline, true, "isMultiline required parameter"); if (typedArray.hasValue(R.styleable.TypefacedEditText_isMultiline)) { @@ -139,12 +138,6 @@ public class TypefacedEditText extends AppCompatEditText { private void checkEditTextSpecificAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes, @NonNull final List errors) throws NoSuchFieldException, IllegalAccessException { - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_typeface"), false, - "remove typeface and use customTypeface"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_textStyle"), false, - "remove textStyle and use customTypeface"); - AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_fontFamily"), false, - "remove fontFamily and use customTypeface"); 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, @@ -198,25 +191,13 @@ public class TypefacedEditText extends AppCompatEditText { } 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 - } - + addTextChangedListener(new DefaultTextWatcher() { @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 - } - }); } @@ -317,15 +298,6 @@ public class TypefacedEditText extends AppCompatEditText { super.setInputType(type); } - /** - * Sets typeface from 'assets/fonts' folder by name. - * - * @param name Full name of typeface (without extension, e.g. 'Roboto-Regular'). - */ - public void setTypeface(@NonNull final String name) { - setTypeface(Typefaces.getByName(getContext(), name)); - } - public void setOnTextChangedListener(@Nullable final OnTextChangedListener onTextChangedListener) { this.onTextChangedListener = onTextChangedListener; } @@ -344,4 +316,4 @@ public class TypefacedEditText extends AppCompatEditText { } -} \ No newline at end of file +} diff --git a/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java b/views/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java similarity index 95% rename from src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java rename to views/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java index 715e77e..0c19b4d 100644 --- a/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java +++ b/views/src/main/java/ru/touchin/roboswag/components/views/TypefacedTextView.java @@ -33,8 +33,6 @@ import android.util.TypedValue; import java.util.ArrayList; import java.util.List; -import ru.touchin.roboswag.components.R; -import ru.touchin.roboswag.components.utils.Typefaces; import ru.touchin.roboswag.components.utils.UiUtils; import ru.touchin.roboswag.components.views.internal.AttributesUtils; import ru.touchin.roboswag.core.log.Lc; @@ -45,6 +43,7 @@ import ru.touchin.roboswag.core.log.Lc; * Also in debug mode it has common checks for popular bugs. */ @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") +@Deprecated //ConstructorCallsOverridableMethod: it's ok as we need to setTypeface public class TypefacedTextView extends AppCompatTextView { @@ -53,15 +52,6 @@ public class TypefacedTextView extends AppCompatTextView { 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; @@ -93,11 +83,8 @@ public class TypefacedTextView extends AppCompatTextView { } else { setLineStrategy(lineStrategy); } - if (!isInEditMode()) { - setTypeface(Typefaces.getFromAttributes(context, attrs, R.styleable.TypefacedTextView, R.styleable.TypefacedTextView_customTypeface)); - } typedArray.recycle(); - if (inDebugMode) { + if (BuildConfig.DEBUG) { checkAttributes(context, attrs); } } @@ -107,8 +94,6 @@ public class TypefacedTextView extends AppCompatTextView { final List errors = new ArrayList<>(); LineStrategy lineStrategy = null; TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TypefacedTextView); - AttributesUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedTextView_customTypeface, true, - "customTypeface required parameter"); AttributesUtils.checkAttribute(typedArray, errors, R.styleable.TypefacedTextView_lineStrategy, true, "lineStrategy required parameter"); if (typedArray.hasValue(R.styleable.TypefacedTextView_lineStrategy)) { @@ -136,6 +121,7 @@ public class TypefacedTextView extends AppCompatTextView { private void checkTextViewSpecificAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes, @NonNull final List errors) throws NoSuchFieldException, IllegalAccessException { + AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_phoneNumber"), false, "phoneNumber forbid parameter"); AttributesUtils.checkAttribute(typedArray, errors, AttributesUtils.getField(androidRes, "TextView_password"), false, @@ -311,15 +297,6 @@ public class TypefacedTextView extends AppCompatTextView { Lc.assertion(new IllegalStateException(AttributesUtils.viewError(this, "Do not specify ellipsize use setLineStrategy instead"))); } - /** - * Sets typeface from 'assets/fonts' folder by name. - * - * @param name Full name of typeface (without extension, e.g. 'Roboto-Regular'). - */ - public void setTypeface(@NonNull final String name) { - setTypeface(Typefaces.getByName(getContext(), name)); - } - @Override public void setText(@Nullable final CharSequence text, @Nullable final BufferType type) { super.setText(text, type); diff --git a/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java b/views/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java similarity index 86% rename from src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java rename to views/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java index e1e8bbe..0c85eac 100644 --- a/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java +++ b/views/src/main/java/ru/touchin/roboswag/components/views/internal/AttributesUtils.java @@ -39,6 +39,7 @@ 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. */ +@Deprecated public final class AttributesUtils { /** @@ -69,11 +70,13 @@ public final class AttributesUtils { * @param required Is parameter have to be in array OR it have not to be in; * @param description Description of error. */ - public static void checkAttribute(@NonNull final TypedArray typedArray, - @NonNull final Collection errors, - @StyleableRes final int resourceId, - final boolean required, - @NonNull final String description) { + public static void checkAttribute( + @NonNull final TypedArray typedArray, + @NonNull final Collection errors, + @StyleableRes final int resourceId, + final boolean required, + @NonNull final String description + ) { if ((required && typedArray.hasValue(resourceId)) || (!required && !typedArray.hasValue(resourceId))) { return; @@ -91,12 +94,14 @@ public final class AttributesUtils { * @throws NoSuchFieldException Throws during getting attribute values through reflection; * @throws IllegalAccessException Throws during getting attribute values through reflection. */ - public static void checkRegularTextViewAttributes(@NonNull final TypedArray typedArray, @NonNull final Class androidRes, - @NonNull final Collection errors, @NonNull final String lineStrategyParameterName) + public static void checkRegularTextViewAttributes( + @NonNull final TypedArray typedArray, + @NonNull final Class androidRes, + @NonNull final Collection errors, + @NonNull final String lineStrategyParameterName + ) throws NoSuchFieldException, IllegalAccessException { - checkAttribute(typedArray, errors, getField(androidRes, "TextView_typeface"), false, "remove typeface and use customTypeface"); - checkAttribute(typedArray, errors, getField(androidRes, "TextView_textStyle"), false, "remove textStyle and use customTypeface"); - checkAttribute(typedArray, errors, getField(androidRes, "TextView_fontFamily"), false, "remove fontFamily and use customTypeface"); + 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); diff --git a/src/main/res/values/attrs.xml b/views/src/main/res/values/attrs.xml similarity index 75% rename from src/main/res/values/attrs.xml rename to views/src/main/res/values/attrs.xml index 263a114..43424f2 100644 --- a/src/main/res/values/attrs.xml +++ b/views/src/main/res/values/attrs.xml @@ -1,10 +1,7 @@ - - - @@ -17,15 +14,9 @@ - - - - - - @@ -33,4 +24,4 @@ - \ No newline at end of file +