Repository: hamidness/restring
Branch: master
Commit: fdd206cf481b
Files: 88
Total size: 215.2 KB
Directory structure:
gitextract_tfw8tuy5/
├── .gitignore
├── .travis.yml
├── README.md
├── build.gradle
├── docs/
│ ├── META-INF/
│ │ └── MANIFEST.MF
│ ├── allclasses-frame.html
│ ├── allclasses-noframe.html
│ ├── com/
│ │ └── ice/
│ │ └── restring/
│ │ ├── Restring.StringsLoader.html
│ │ ├── Restring.html
│ │ ├── RestringConfig.Builder.html
│ │ ├── RestringConfig.html
│ │ ├── package-frame.html
│ │ ├── package-summary.html
│ │ └── package-tree.html
│ ├── constant-values.html
│ ├── deprecated-list.html
│ ├── help-doc.html
│ ├── index-all.html
│ ├── index.html
│ ├── overview-tree.html
│ ├── package-list
│ ├── script.js
│ └── stylesheet.css
├── example/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── ice/
│ │ └── restring/
│ │ └── example/
│ │ ├── BaseActivity.java
│ │ ├── MainActivity.java
│ │ ├── SampleApplication.java
│ │ └── SampleStringsLoader.java
│ └── res/
│ ├── drawable/
│ │ └── ic_launcher_background.xml
│ ├── drawable-v24/
│ │ └── ic_launcher_foreground.xml
│ ├── layout/
│ │ └── activity_main.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ └── values/
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── restring/
│ ├── .gitignore
│ ├── bintray.gradle
│ ├── build.gradle
│ ├── install.gradle
│ ├── jacoco.gradle
│ ├── proguard-rules.pro
│ ├── publish.gradle
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── ice/
│ │ │ └── restring/
│ │ │ ├── BottomNavigationViewTransformer.java
│ │ │ ├── CustomResourcesContextWrapper.java
│ │ │ ├── MemoryStringRepository.java
│ │ │ ├── ReflectionUtils.java
│ │ │ ├── Restring.java
│ │ │ ├── RestringConfig.java
│ │ │ ├── RestringContextWrapper.java
│ │ │ ├── RestringLayoutInflater.java
│ │ │ ├── RestringResources.java
│ │ │ ├── RestringUtil.java
│ │ │ ├── SharedPrefStringRepository.java
│ │ │ ├── StringRepository.java
│ │ │ ├── StringsLoaderTask.java
│ │ │ ├── SupportToolbarTransformer.java
│ │ │ ├── TextViewTransformer.java
│ │ │ ├── ToolbarTransformer.java
│ │ │ └── ViewTransformerManager.java
│ │ └── res/
│ │ ├── layout/
│ │ │ └── test_layout.xml
│ │ ├── menu/
│ │ │ └── test_menu.xml
│ │ └── values/
│ │ └── strings.xml
│ └── test/
│ └── java/
│ └── com/
│ └── ice/
│ └── restring/
│ ├── MemoryStringRepositoryTest.java
│ ├── RestringContextWrapperTest.java
│ ├── RestringLayoutInflaterTest.java
│ ├── RestringResourcesTest.java
│ ├── RestringTest.java
│ ├── SharedPrefStringRepositoryTest.java
│ ├── StringsLoaderTaskTest.java
│ ├── SupportToolbarTransformerTest.java
│ ├── TextViewTransformerTest.java
│ ├── ToolbarTransformerTest.java
│ ├── ViewTransformerManagerTest.java
│ ├── activity/
│ │ └── TestActivity.java
│ └── shadow/
│ ├── MyShadowAssetManager.java
│ └── MyShadowAsyncTask.java
├── settings.gradle
└── update_javadoc.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
.DS_Store
/build
/captures
.externalNativeBuild
================================================
FILE: .travis.yml
================================================
language: android
jdk: oraclejdk8
env:
global:
- ANDROID_TARGET=android-16
- ANDROID_ABI=armeabi-v7a
- BUILD_TOOLS_VERSION=27.0.3
- ANDROID_TARGET=27
android:
components:
- tools
- platform-tools
- build-tools-${BUILD_TOOLS_VERSION}
- android-${ANDROID_TARGET}
- extra-google-m2repository
- extra-android-m2repository
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
script:
- ./gradlew build jacocoTestReport
after_success:
- bash <(curl -s https://codecov.io/bash)
================================================
FILE: README.md
================================================
[](http://androidweekly.net/issues/issue-307)
### Please use this version instead, if you're going to use thie library.
## Restring 1.0
An easy way to replace bundled Strings dynamically, or provide new translations in Android
### 1. Add dependency
```groovy
implementation 'com.ice.restring:restring:1.0.0'
```
### 2. Initialize
Initialize Restring in your Application class:
```java
Restring.init(context);
```
or if you want more configurations:
```java
Restring.init(context,
new RestringConfig.Builder()
.persist(true)
.stringsLoader(new SampleStringsLoader())
.build()
);
```
### 3. Inject into Context
if you have a BaseActivity you can add this there, otherwise you have to add it to all of your activities!
```java
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(Restring.wrapContext(newBase));
}
```
### 4. Provide new Strings
There're two ways to provide new Strings. You can use either way or both.
First way: You can implement Restring.StringsLoader like this:
```java
public class MyStringsLoader implements Restring.StringsLoader {
//This will be called on background thread.
@Override
public List getLanguages() {
//return your supported languages(e.g. "en", ...)
}
//This will be called on background thread.
@Override
public Map getStrings(String language) {
Map map = new HashMap<>();
// Load your strings here into a map of (key,value)s for this language!
return map;
}
}
```
and initialize Restring like this:
```java
Restring.init(context,
new RestringConfig.Builder()
.persist(true)
.stringsLoader(new MyStringsLoader())
.build()
);
```
Second way:
Load your Strings in any way / any time / any place and just call this:
```java
// e.g. language="en" newStrings=map of (key-value)s
Restring.setStrings(language, newStrings);
```
### 5. Done!
Now all strings in your app will be overriden by new strings provided to Restring.
## Notes:
1. Please note that Restring works with current locale, so if you change locale with
```java
Locale.setDefault(newLocale);
```
Restring will start using strings of the new locale.
2. By default, Restring will use shared preferences to save all strings provided to. So if you set a StringsLoader or call .setString() to set the strings into Restring, the strings will be there on the next application launch. In case you don't want Restring saves strings into shared preferences, you can set it in initialization, like this:
```java
Restring.init(context,
new RestringConfig.Builder()
.persist(false) //Set this to false to prevent saving into shared preferences.
.build()
);
```
3. For displaying a string, Restring tries to find that in dynamic strings, and will use bundled version as fallback. In the other words, Only the new provided strings will be overriden and for the rest the bundled version will be used.
## Limitations
1. Plurals are not supported yet.
2. String arrays are not supported yet.
## Docs
* Medium
* Javadocs
## License
Copyright 2018 Hamid Gharehdaghi
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.
Loader of strings skeleton. Clients can implement this interface if they want to load strings on initialization.
First the list of languages will be asked, then strings of each language.
================================================
FILE: docs/help-doc.html
================================================
API Help (restring 1.0.0 API)
This API (Application Programming Interface) document has pages corresponding to the items in the navigation bar, described as follows.
Package
Each package has a page that contains a list of its classes and interfaces, with a summary for each. This page can contain six categories:
Interfaces (italic)
Classes
Enums
Exceptions
Errors
Annotation Types
Class/Interface
Each class, interface, nested class and nested interface has its own separate page. Each of these pages has three sections consisting of a class/interface description, summary tables, and detailed member descriptions:
Class inheritance diagram
Direct Subclasses
All Known Subinterfaces
All Known Implementing Classes
Class/interface declaration
Class/interface description
Nested Class Summary
Field Summary
Constructor Summary
Method Summary
Field Detail
Constructor Detail
Method Detail
Each summary entry contains the first sentence from the detailed description for that item. The summary entries are alphabetical, while the detailed descriptions are in the order they appear in the source code. This preserves the logical groupings established by the programmer.
Annotation Type
Each annotation type has its own separate page with the following sections:
Annotation Type declaration
Annotation Type description
Required Element Summary
Optional Element Summary
Element Detail
Enum
Each enum has its own separate page with the following sections:
Enum declaration
Enum description
Enum Constant Summary
Enum Constant Detail
Tree (Class Hierarchy)
There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. The classes are organized by inheritance structure starting with java.lang.Object. The interfaces do not inherit from java.lang.Object.
When viewing the Overview page, clicking on "Tree" displays the hierarchy for all packages.
When viewing a particular package, class or interface page, clicking "Tree" displays the hierarchy for only that package.
Deprecated API
The Deprecated API page lists all of the API that have been deprecated. A deprecated API is not recommended for use, generally due to improvements, and a replacement API is usually given. Deprecated APIs may be removed in future implementations.
Index
The Index contains an alphabetic list of all classes, interfaces, constructors, methods, and fields.
Prev/Next
These links take you to the next or previous class, interface, package, or related page.
Frames/No Frames
These links show and hide the HTML frames. All pages are available with or without frames.
All Classes
The All Classes link shows all classes and interfaces except non-static nested types.
Serialized Form
Each serializable or externalizable class has a description of its serialization fields and methods. This information is of interest to re-implementors, not to developers using the API. While there is no link in the navigation bar, you can get to this information by going to any serialized class and clicking "Serialized Form" in the "See also" section of the class description.
* All overridden methods will be called on background thread.
*/
public class SampleStringsLoader implements Restring.StringsLoader {
@Override
public List getLanguages() {
return Arrays.asList("en", "de", "fa");
}
@Override
public Map getStrings(String language) {
Map map = new HashMap<>();
switch (language) {
case "en": {
map.put("title", "This is title (from restring).");
map.put("subtitle", "This is subtitle (from restring).");
break;
}
case "de": {
map.put("title", "Das ist Titel (from restring).");
map.put("subtitle", "Das ist Untertitel (from restring).");
break;
}
case "fa": {
map.put("title", "In sarkhat ast (from restring).");
map.put("subtitle", "In matn ast (from restring).");
break;
}
}
return map;
}
}
================================================
FILE: example/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: example/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: example/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: example/src/main/res/values/colors.xml
================================================
#3F51B5#303F9F#FF4081
================================================
FILE: example/src/main/res/values/strings.xml
================================================
RestringThis is title.This is subtitle.
================================================
FILE: example/src/main/res/values/styles.xml
================================================
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Thu Mar 15 15:49:17 CET 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip
================================================
FILE: gradle.properties
================================================
# 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
================================================
FILE: gradlew
================================================
#!/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" "$@"
================================================
FILE: gradlew.bat
================================================
@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
================================================
FILE: restring/.gitignore
================================================
/build
================================================
FILE: restring/bintray.gradle
================================================
apply plugin: 'com.jfrog.bintray'
version = libraryVersion
/*
* Comment the following part if you only want to distribute .aar files.
* (For example, your source code is obfuscated by Proguard and is not shown to outsite developers)
* Without source code .jar files, you only can publish on bintray respository but not jcenter.
*/
/*--------------------------------*/
if (project.hasProperty("android")) {
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.srcDirs
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.java.srcDirs
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}
afterEvaluate {
javadoc.classpath += files(android.libraryVariants.collect { variant ->
variant.javaCompiler.classpath.files
})
}
} else { // Java libraries
task sourcesJar(type: Jar, dependsOn: classes) {
classifier = 'sources'
from sourceSets.main.allSource
}
}
task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from javadoc.destinationDir
}
artifacts {
archives javadocJar
archives sourcesJar
}
/*--------------------------------*/
// Bintray
String localProperty(propertyName) {
Properties properties = new Properties()
File file = project.rootProject.file('local.properties')
if (file.exists()) {
properties.load(file.newDataInputStream())
return properties.getProperty(propertyName)
}
return ""
}
bintray {
user = localProperty("bintrayUser")
key = localProperty("bintrayApiKey")
configurations = ['archives']
pkg {
repo = bintrayRepo
name = bintrayName
desc = libraryDescription
userOrg = organization
// If the repository is hosted by an organization instead of personal account.
websiteUrl = siteUrl
vcsUrl = gitUrl
licenses = allLicenses
publish = true
override = true
publicDownloadNumbers = true
version {
desc = libraryDescription
}
}
}
================================================
FILE: restring/build.gradle
================================================
apply plugin: 'com.android.library'
apply from: 'jacoco.gradle'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
debug {
minifyEnabled false
testCoverageEnabled true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
// Support libraries
implementation deps.supportAppCompat
implementation deps.supportDesign
// Test libraries
testImplementation deps.junit
testImplementation deps.hamcrest
testImplementation deps.mockito
testImplementation deps.robolectric
}
apply from: 'publish.gradle'
================================================
FILE: restring/install.gradle
================================================
apply plugin: 'com.github.dcendents.android-maven'
group = publishedGroupId // Maven Group ID for the artifact
install {
repositories.mavenInstaller {
// This generates POM.xml with proper parameters
pom.project {
packaging 'aar'
groupId publishedGroupId
artifactId artifact
// Add your description here
name libraryName
description libraryDescription
url siteUrl
// Set your license
licenses {
license {
name licenseName
url licenseUrl
}
}
developers {
developer {
id developerId
name developerName
email developerEmail
}
}
scm {
connection gitUrl
developerConnection gitUrl
url siteUrl
}
}
}
}
================================================
FILE: restring/jacoco.gradle
================================================
apply plugin: 'jacoco'
jacoco {
toolVersion = '0.8.0'
}
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
}
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {
reports {
xml.enabled = true
html.enabled = true
}
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*']
def debugTree = fileTree(dir: "$project.buildDir/intermediates/artifact_transform/compileDebugJavaWithJavac/classes", excludes: fileFilter)
def mainSrc = "$project.projectDir/src/main/java"
sourceDirectories = files([mainSrc])
classDirectories = files([debugTree])
executionData = fileTree(dir: project.buildDir, includes: [
'jacoco/testDebugUnitTest.exec', 'outputs/code-coverage/connected/*coverage.ec'
])
}
================================================
FILE: restring/proguard-rules.pro
================================================
# 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
================================================
FILE: restring/publish.gradle
================================================
ext {
bintrayRepo = 'maven'
bintrayName = 'restring'
publishedGroupId = 'com.ice.restring'
artifact = 'restring'
libraryVersion = '1.0.0'
libraryName = 'Restring'
libraryDescription = 'Replace Strings dynamically, or provide new translations, for Android'
siteUrl = 'https://github.com/hamidness/restring'
gitUrl = 'https://github.com/hamidness/restring.git'
licenseName = 'The Apache Software License, Version 2.0'
licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
allLicenses = ["Apache-2.0"]
developerId = 'hamidfri'
developerName = 'Hamid Gharehdaghi'
developerEmail = 'hamidice@gmail.com'
organization = ''
}
apply from: 'install.gradle'
apply from: 'bintray.gradle'
================================================
FILE: restring/src/main/AndroidManifest.xml
================================================
================================================
FILE: restring/src/main/java/com/ice/restring/BottomNavigationViewTransformer.java
================================================
package com.ice.restring;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.support.design.widget.BottomNavigationView;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.Xml;
import android.view.View;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* A transformer which transforms BottomNavigationView: it transforms the texts coming from the menu.
*/
class BottomNavigationViewTransformer implements ViewTransformerManager.Transformer {
private static final String ATTRIBUTE_MENU = "menu";
private static final String ATTRIBUTE_APP_MENU = "app:menu";
private static final String ATTRIBUTE_ID = "id";
private static final String ATTRIBUTE_ANDROID_ID = "android:id";
private static final String ATTRIBUTE_TITLE = "title";
private static final String ATTRIBUTE_ANDROID_TITLE = "android:title";
private static final String ATTRIBUTE_TITLE_CONDENSED = "titleCondensed";
private static final String ATTRIBUTE_ANDROID_TITLE_CONDENSED = "android:titleCondensed";
private static final String XML_MENU = "menu";
private static final String XML_ITEM = "item";
@Override
public Class extends View> getViewType() {
return BottomNavigationView.class;
}
@Override
public View transform(View view, AttributeSet attrs) {
if (view == null || !getViewType().isInstance(view)) {
return view;
}
Resources resources = view.getContext().getResources();
BottomNavigationView bottomNavigationView = (BottomNavigationView) view;
for (int index = 0; index < attrs.getAttributeCount(); index++) {
String attributeName = attrs.getAttributeName(index);
switch (attributeName) {
case ATTRIBUTE_APP_MENU:
case ATTRIBUTE_MENU: {
String value = attrs.getAttributeValue(index);
if (value == null || !value.startsWith("@")) break;
int resId = attrs.getAttributeResourceValue(index, 0);
Map itemStrings = getMenuItemsStrings(resources, resId);
for (Map.Entry entry : itemStrings.entrySet()) {
if (entry.getValue().title != 0) {
bottomNavigationView.getMenu().findItem(entry.getKey()).setTitle(
resources.getString(entry.getValue().title)
);
}
if (entry.getValue().titleCondensed != 0) {
bottomNavigationView.getMenu().findItem(entry.getKey()).setTitleCondensed(
resources.getString(entry.getValue().titleCondensed)
);
}
}
break;
}
}
}
return view;
}
private Map getMenuItemsStrings(Resources resources, int resId) {
XmlResourceParser parser = resources.getLayout(resId);
AttributeSet attrs = Xml.asAttributeSet(parser);
try {
return parseMenu(parser, attrs);
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
return new HashMap<>();
}
}
private Map parseMenu(XmlPullParser parser, AttributeSet attrs)
throws XmlPullParserException, IOException {
Map menuItems = new HashMap<>();
int eventType = parser.getEventType();
String tagName;
// This loop will skip to the menu start tag
do {
if (eventType == XmlPullParser.START_TAG) {
tagName = parser.getName();
if (tagName.equals(XML_MENU)) {
eventType = parser.next();
break;
}
throw new RuntimeException("Expecting menu, got " + tagName);
}
eventType = parser.next();
} while (eventType != XmlPullParser.END_DOCUMENT);
boolean reachedEndOfMenu = false;
int menuLevel = 0;
while (!reachedEndOfMenu) {
switch (eventType) {
case XmlPullParser.START_TAG:
tagName = parser.getName();
if (tagName.equals(XML_ITEM)) {
Pair item = parseMenuItem(attrs);
if (item != null) {
menuItems.put(item.first, item.second);
}
} else if (tagName.equals(XML_MENU)) {
menuLevel++;
}
break;
case XmlPullParser.END_TAG:
tagName = parser.getName();
if (tagName.equals(XML_MENU)) {
menuLevel--;
if (menuLevel <= 0) {
reachedEndOfMenu = true;
}
}
break;
case XmlPullParser.END_DOCUMENT:
reachedEndOfMenu = true;
}
eventType = parser.next();
}
return menuItems;
}
private Pair parseMenuItem(AttributeSet attrs) {
int menuId = 0;
MenuItemStrings menuItemStrings = null;
int attributeCount = attrs.getAttributeCount();
for (int index = 0; index < attributeCount; index++) {
switch (attrs.getAttributeName(index)) {
case ATTRIBUTE_ANDROID_ID:
case ATTRIBUTE_ID: {
menuId = attrs.getAttributeResourceValue(index, 0);
break;
}
case ATTRIBUTE_ANDROID_TITLE:
case ATTRIBUTE_TITLE: {
String value = attrs.getAttributeValue(index);
if (value == null || !value.startsWith("@")) break;
if (menuItemStrings == null) {
menuItemStrings = new MenuItemStrings();
}
menuItemStrings.title = attrs.getAttributeResourceValue(index, 0);
break;
}
case ATTRIBUTE_ANDROID_TITLE_CONDENSED:
case ATTRIBUTE_TITLE_CONDENSED: {
String value = attrs.getAttributeValue(index);
if (value == null || !value.startsWith("@")) break;
if (menuItemStrings == null) {
menuItemStrings = new MenuItemStrings();
}
menuItemStrings.titleCondensed = attrs.getAttributeResourceValue(index, 0);
break;
}
}
}
return (menuId != 0 && menuItemStrings != null)
? new Pair<>(menuId, menuItemStrings)
: null;
}
private static class MenuItemStrings {
public int title;
public int titleCondensed;
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/CustomResourcesContextWrapper.java
================================================
package com.ice.restring;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Resources;
/**
* A context wrapper which provide another Resources instead of the original one.
*/
class CustomResourcesContextWrapper extends ContextWrapper {
private Resources resources;
public CustomResourcesContextWrapper(Context base, Resources resources) {
super(base);
this.resources = resources;
}
@Override
public Resources getResources() {
return resources;
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/MemoryStringRepository.java
================================================
package com.ice.restring;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A StringRepository which keeps the strings ONLY in memory.
*
* it's not ThreadSafe.
*/
class MemoryStringRepository implements StringRepository {
private Map> strings = new LinkedHashMap<>();
@Override
public void setStrings(String language, Map newStrings) {
strings.put(language, newStrings);
}
@Override
public void setString(String language, String key, String value) {
if (!strings.containsKey(language)) {
strings.put(language, new LinkedHashMap<>());
}
strings.get(language).put(key, value);
}
@Override
public String getString(String language, String key) {
if (!strings.containsKey(language) || !strings.get(language).containsKey(key)) {
return null;
}
return strings.get(language).get(key);
}
@Override
public Map getStrings(String language) {
if (!strings.containsKey(language)) {
return new LinkedHashMap<>();
}
return new LinkedHashMap<>(strings.get(language));
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/ReflectionUtils.java
================================================
package com.ice.restring;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* Created by chris on 17/12/14.
* Copied from Calligraphy:
* https://github.com/chrisjenx/Calligraphy/blob/master/calligraphy/src/main/java/uk/co/chrisjenx/calligraphy/ReflectionUtils.java
*/
class ReflectionUtils {
private static final String TAG = ReflectionUtils.class.getSimpleName();
static Field getField(Class clazz, String fieldName) {
try {
final Field f = clazz.getDeclaredField(fieldName);
f.setAccessible(true);
return f;
} catch (NoSuchFieldException ignored) {
}
return null;
}
static Object getValue(Field field, Object obj) {
try {
return field.get(obj);
} catch (IllegalAccessException ignored) {
}
return null;
}
static void setValue(Field field, Object obj, Object value) {
try {
field.set(obj, value);
} catch (IllegalAccessException ignored) {
}
}
static Method getMethod(Class clazz, String methodName) {
final Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
method.setAccessible(true);
return method;
}
}
return null;
}
static void invokeMethod(Object object, Method method, Object... args) {
try {
if (method == null) return;
method.invoke(object, args);
} catch (IllegalAccessException e) {
Log.d(TAG, "Can't invoke method using reflection", e);
} catch (InvocationTargetException e) {
Log.d(TAG, "Can't invoke method using reflection", e);
}
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/Restring.java
================================================
package com.ice.restring;
import android.content.Context;
import android.content.ContextWrapper;
import java.util.List;
import java.util.Map;
/**
* Entry point for Restring. it will be used for initializing Restring components, setting new strings,
* wrapping activity context.
*/
public abstract class Restring {
private static boolean isInitialized = false;
private static StringRepository stringRepository;
private static ViewTransformerManager viewTransformerManager;
/**
* Initialize Restring with default configuration.
*
* @param context of the application.
*/
public static void init(Context context) {
init(context, RestringConfig.getDefault());
}
/**
* Initialize Restring with the specified configuration.
*
* @param context of the application.
* @param config of the Restring.
*/
public static void init(Context context, RestringConfig config) {
if (isInitialized) {
return;
}
isInitialized = true;
initStringRepository(context, config);
initViewTransformer();
}
/**
* Wraps context of an activity to provide Restring features.
*
* @param base context of an activity.
* @return the Restring wrapped context.
*/
public static ContextWrapper wrapContext(Context base) {
return RestringContextWrapper.wrap(base, stringRepository, viewTransformerManager);
}
/**
* Set strings of a language.
*
* @param language the strings are for.
* @param newStrings the strings of the language.
*/
public static void setStrings(String language, Map newStrings) {
stringRepository.setStrings(language, newStrings);
}
/**
* Set a single string for a language.
*
* @param language the string is for.
* @param key the string key.
* @param value the string value.
*/
public static void setString(String language, String key, String value) {
stringRepository.setString(language, key, value);
}
static StringRepository getStringRepository() {
return stringRepository;
}
private static void initStringRepository(Context context, RestringConfig config) {
if (config.isPersist()) {
stringRepository = new SharedPrefStringRepository(context);
} else {
stringRepository = new MemoryStringRepository();
}
if (config.getStringsLoader() != null) {
new StringsLoaderTask(config.getStringsLoader(), stringRepository).run();
}
}
private static void initViewTransformer() {
viewTransformerManager = new ViewTransformerManager();
viewTransformerManager.registerTransformer(new TextViewTransformer());
viewTransformerManager.registerTransformer(new ToolbarTransformer());
viewTransformerManager.registerTransformer(new SupportToolbarTransformer());
viewTransformerManager.registerTransformer(new BottomNavigationViewTransformer());
}
/**
* Loader of strings skeleton. Clients can implement this interface if they want to load strings on initialization.
* First the list of languages will be asked, then strings of each language.
*/
public interface StringsLoader {
/**
* Get supported languages.
*
* @return the list of languages.
*/
List getLanguages();
/**
* Get strings of a language as keys & values.
*
* @param language of the strings.
* @return the strings as (key, value).
*/
Map getStrings(String language);
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/RestringConfig.java
================================================
package com.ice.restring;
/**
* Contains configuration properties for initializing Restring.
*/
public class RestringConfig {
private boolean persist;
private Restring.StringsLoader stringsLoader;
public boolean isPersist() {
return persist;
}
public Restring.StringsLoader getStringsLoader() {
return stringsLoader;
}
private RestringConfig() {
}
public static class Builder {
private boolean persist;
private Restring.StringsLoader stringsLoader;
public Builder persist(boolean persist) {
this.persist = persist;
return this;
}
public Builder stringsLoader(Restring.StringsLoader loader) {
this.stringsLoader = loader;
return this;
}
public RestringConfig build() {
RestringConfig config = new RestringConfig();
config.persist = persist;
config.stringsLoader = stringsLoader;
return config;
}
}
static RestringConfig getDefault() {
return new Builder()
.persist(true)
.build();
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/RestringContextWrapper.java
================================================
package com.ice.restring;
import android.content.Context;
import android.content.ContextWrapper;
import android.view.LayoutInflater;
/**
* Main Restring context wrapper which wraps the context for providing another layout inflater & resources.
*/
class RestringContextWrapper extends ContextWrapper {
private RestringLayoutInflater layoutInflater;
private ViewTransformerManager viewTransformerManager;
public static RestringContextWrapper wrap(Context context,
StringRepository stringRepository,
ViewTransformerManager viewTransformerManager) {
return new RestringContextWrapper(context, stringRepository, viewTransformerManager);
}
private RestringContextWrapper(Context base,
StringRepository stringRepository,
ViewTransformerManager viewTransformerManager) {
super(
new CustomResourcesContextWrapper(
base,
new RestringResources(base.getResources(), stringRepository))
);
this.viewTransformerManager = viewTransformerManager;
}
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (layoutInflater == null) {
layoutInflater = new RestringLayoutInflater(LayoutInflater.from(getBaseContext()), this, viewTransformerManager, true);
}
return layoutInflater;
}
return super.getSystemService(name);
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/RestringLayoutInflater.java
================================================
package com.ice.restring;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.xmlpull.v1.XmlPullParser;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* Restring custom layout inflater. it puts hook on view creation, and tries to apply some transformations
* to the newly created views.
*
* Transformations can consist of transforming the texts applied on XML layout resources, so that it checks if
* the string attribute set as a string resource it transforms the text and apply it to the view again.
*/
class RestringLayoutInflater extends LayoutInflater {
private boolean privateFactorySet = false;
private Field mConstructorArgs = null;
private ViewTransformerManager viewTransformerManager;
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
protected RestringLayoutInflater(Context context) {
super(context);
initFactories();
}
RestringLayoutInflater(LayoutInflater original,
Context newContext,
ViewTransformerManager viewTransformerManager,
final boolean cloned) {
super(original, newContext);
this.viewTransformerManager = viewTransformerManager;
if (!cloned) {
initFactories();
}
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new RestringLayoutInflater(this, newContext, viewTransformerManager, true);
}
private void initFactories() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
if (getFactory2() != null) {
setFactory2(getFactory2());
}
}
if (getFactory() != null) {
setFactory(getFactory());
}
}
@Override
public void setFactory(Factory factory) {
if (!(factory instanceof WrapperFactory)) {
super.setFactory(new WrapperFactory(factory));
} else {
super.setFactory(factory);
}
}
@Override
public void setFactory2(Factory2 factory2) {
if (!(factory2 instanceof WrapperFactory2)) {
super.setFactory2(new WrapperFactory2(factory2));
} else {
super.setFactory2(factory2);
}
}
private void setPrivateFactoryInternal() {
if (privateFactorySet) return;
if (!(getContext() instanceof Factory2)) {
privateFactorySet = true;
return;
}
final Method setPrivateFactoryMethod = ReflectionUtils
.getMethod(LayoutInflater.class, "setPrivateFactory");
if (setPrivateFactoryMethod != null) {
PrivateWrapperFactory2 newFactory = new PrivateWrapperFactory2((Factory2) getContext());
ReflectionUtils.invokeMethod(
this,
setPrivateFactoryMethod,
newFactory);
}
privateFactorySet = true;
}
@Override
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
setPrivateFactoryInternal();
return super.inflate(parser, root, attachToRoot);
}
@Override
protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return applyChange(view, attrs);
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
private View applyChange(View view, AttributeSet attrs) {
return viewTransformerManager.transform(view, attrs);
}
private class WrapperFactory implements Factory {
private Factory factory;
WrapperFactory(Factory factory) {
this.factory = factory;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = factory.onCreateView(name, context, attrs);
return applyChange(view, attrs);
}
}
private class WrapperFactory2 implements Factory2 {
private Factory2 factory2;
WrapperFactory2(Factory2 factory2) {
this.factory2 = factory2;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = factory2.onCreateView(parent, name, context, attrs);
return applyChange(view, attrs);
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = factory2.onCreateView(name, context, attrs);
return applyChange(view, attrs);
}
}
private View createCustomViewInternal(View parent, View view, String name, Context viewContext, AttributeSet attrs) {
// I by no means advise anyone to do this normally, but Google have locked down access to
// the createView() method, so we never get a callback with attributes at the end of the
// createViewFromTag chain (which would solve all this unnecessary rubbish).
// We at the very least try to optimise this as much as possible.
// We only call for customViews (As they are the ones that never go through onCreateView(...)).
// We also maintain the Field reference and make it accessible which will make a pretty
// significant difference to performance on Android 4.0+.
// If CustomViewCreation is off skip this.
if (view == null && name.indexOf('.') > -1) {
if (mConstructorArgs == null)
mConstructorArgs = ReflectionUtils.getField(LayoutInflater.class, "mConstructorArgs");
final Object[] mConstructorArgsArr = (Object[]) ReflectionUtils.getValue(mConstructorArgs, this);
final Object lastContext = mConstructorArgsArr[0];
// The LayoutInflater actually finds out the correct context to use. We just need to set
// it on the mConstructor for the internal method.
// Set the constructor ars up for the createView, not sure why we can't pass these in.
mConstructorArgsArr[0] = viewContext;
ReflectionUtils.setValue(mConstructorArgs, this, mConstructorArgsArr);
try {
view = createView(name, null, attrs);
} catch (ClassNotFoundException ignored) {
} finally {
mConstructorArgsArr[0] = lastContext;
ReflectionUtils.setValue(mConstructorArgs, this, mConstructorArgsArr);
}
}
return view;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private class PrivateWrapperFactory2 implements Factory2 {
private Factory2 factory2;
public PrivateWrapperFactory2(Factory2 factory2) {
this.factory2 = factory2;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = factory2.onCreateView(parent, name, context, attrs);
view = createCustomViewInternal(parent, view, name, context, attrs);
return applyChange(view, attrs);
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = factory2.onCreateView(name, context, attrs);
view = createCustomViewInternal(null, view, name, context, attrs);
return applyChange(view, attrs);
}
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/RestringResources.java
================================================
package com.ice.restring;
import android.content.res.Resources;
import android.os.Build;
import android.support.annotation.NonNull;
import android.text.Html;
/**
* This is the wrapped resources which will be provided by Restring.
* For getting strings and texts, it checks the strings repository first and if there's a new string
* that will be returned, otherwise it will fallback to the original resource strings.
*/
class RestringResources extends Resources {
private final StringRepository stringRepository;
RestringResources(@NonNull final Resources res,
@NonNull final StringRepository stringRepository) {
super(res.getAssets(), res.getDisplayMetrics(), res.getConfiguration());
this.stringRepository = stringRepository;
}
@NonNull
@Override
public String getString(int id) throws NotFoundException {
String value = getStringFromRepository(id);
if (value != null) {
return value;
}
return super.getString(id);
}
@NonNull
@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
String value = getStringFromRepository(id);
if (value != null) {
return String.format(value, formatArgs);
}
return super.getString(id, formatArgs);
}
@NonNull
@Override
public CharSequence getText(int id) throws NotFoundException {
String value = getStringFromRepository(id);
if (value != null) {
return fromHtml(value);
}
return super.getText(id);
}
@Override
public CharSequence getText(int id, CharSequence def) {
String value = getStringFromRepository(id);
if (value != null) {
return fromHtml(value);
}
return super.getText(id, def);
}
private String getStringFromRepository(int id) {
try {
String stringKey = getResourceEntryName(id);
return stringRepository.getString(RestringUtil.getCurrentLanguage(), stringKey);
} catch (NotFoundException ex) {
return null;
}
}
private CharSequence fromHtml(String source) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
//noinspection deprecation
return Html.fromHtml(source);
} else {
return Html.fromHtml(source, Html.FROM_HTML_MODE_COMPACT);
}
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/RestringUtil.java
================================================
package com.ice.restring;
import java.util.Locale;
class RestringUtil {
static String getCurrentLanguage() {
return Locale.getDefault().getLanguage();
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/SharedPrefStringRepository.java
================================================
package com.ice.restring;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A StringRepository which saves/loads the strings in Shared Preferences.
* it also keeps the strings in memory by using MemoryStringRepository internally for faster access.
*
* it's not ThreadSafe.
*/
class SharedPrefStringRepository implements StringRepository {
private static final String SHARED_PREF_NAME = "Restrings";
private SharedPreferences sharedPreferences;
private StringRepository memoryStringRepository = new MemoryStringRepository();
SharedPrefStringRepository(Context context) {
initSharedPreferences(context);
loadStrings();
}
@Override
public void setStrings(String language, Map strings) {
memoryStringRepository.setStrings(language, strings);
saveStrings(language, strings);
}
@Override
public void setString(String language, String key, String value) {
memoryStringRepository.setString(language, key, value);
Map keyValues = memoryStringRepository.getStrings(language);
keyValues.put(key, value);
saveStrings(language, keyValues);
}
@Override
public String getString(String language, String key) {
return memoryStringRepository.getString(language, key);
}
@Override
public Map getStrings(String language) {
return memoryStringRepository.getStrings(language);
}
private void initSharedPreferences(Context context) {
if (sharedPreferences == null) {
sharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
}
}
private void loadStrings() {
Map strings = sharedPreferences.getAll();
for (Map.Entry entry : strings.entrySet()) {
if (!(entry.getValue() instanceof String)) {
continue;
}
String value = (String) entry.getValue();
Map keyValues = deserializeKeyValues(value);
String language = entry.getKey();
memoryStringRepository.setStrings(language, keyValues);
}
}
private void saveStrings(String language, Map strings) {
String content = serializeKeyValues(strings);
sharedPreferences.edit()
.putString(language, content)
.apply();
}
private Map deserializeKeyValues(String content) {
Map keyValues = new LinkedHashMap<>();
String[] items = content.split(",");
for (String item : items) {
String[] itemKeyValue = item.split("=");
keyValues.put(itemKeyValue[0], itemKeyValue[1].replaceAll(",,", ","));
}
return keyValues;
}
private String serializeKeyValues(Map keyValues) {
StringBuilder content = new StringBuilder();
for (Map.Entry item : keyValues.entrySet()) {
content.append(item.getKey())
.append("=")
.append(item.getValue().replaceAll(",", ",,"))
.append(",");
}
content.deleteCharAt(content.length() - 1);
return content.toString();
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/StringRepository.java
================================================
package com.ice.restring;
import java.util.Map;
/**
* Repository of strings.
*/
interface StringRepository {
/**
* Set strings(key, value) for a specific language.
*
* @param language the strings belongs to.
* @param strings new strings for the language.
*/
void setStrings(String language, Map strings);
/**
* set a single string(key, value) for a specific language.
*
* @param language the string belongs to.
* @param key the key of the string which is the string resource id.
* @param value the new string.
*/
void setString(String language, String key, String value);
/**
* Get a string for a language & key.
*
* @param language the language of the string.
* @param key the string resource id.
* @return the string if exists, otherwise NULL.
*/
String getString(String language, String key);
/**
* Get all strings for a specific language.
*
* @param language the lanugage of the strings.
* @return the map of string key & values. return empty map if there's no.
*/
Map getStrings(String language);
}
================================================
FILE: restring/src/main/java/com/ice/restring/StringsLoaderTask.java
================================================
package com.ice.restring;
import android.os.AsyncTask;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Try to load all strings for different languages by a StringsLoader.
* All string loads happen on background thread, and saving into repository happens on main thread.
*
* FIRST it retrieves all supported languages,
* THEN it retrieves all strings(key, value) for each language.
*/
class StringsLoaderTask extends AsyncTask>> {
private Restring.StringsLoader stringsLoader;
private StringRepository stringRepository;
StringsLoaderTask(Restring.StringsLoader stringsLoader,
StringRepository stringRepository) {
this.stringsLoader = stringsLoader;
this.stringRepository = stringRepository;
}
public void run() {
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
protected Map> doInBackground(Void... voids) {
Map> langStrings = new LinkedHashMap<>();
List languages = stringsLoader.getLanguages();
for (String lang : languages) {
Map keyValues = stringsLoader.getStrings(lang);
if (keyValues != null && keyValues.size() > 0) {
langStrings.put(lang, keyValues);
}
}
return langStrings;
}
@Override
protected void onPostExecute(Map> langStrings) {
for (Map.Entry> langItem : langStrings.entrySet()) {
stringRepository.setStrings(langItem.getKey(), langItem.getValue());
}
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/SupportToolbarTransformer.java
================================================
package com.ice.restring;
import android.content.res.Resources;
import android.support.v7.widget.Toolbar;
import android.util.AttributeSet;
import android.view.View;
/**
* A transformer which transforms Toolbar(from support library): it transforms the text set as title.
*/
class SupportToolbarTransformer implements ViewTransformerManager.Transformer {
private static final String ATTRIBUTE_TITLE = "title";
private static final String ATTRIBUTE_APP_TITLE = "app:title";
@Override
public Class extends View> getViewType() {
return Toolbar.class;
}
@Override
public View transform(View view, AttributeSet attrs) {
if (view == null || !getViewType().isInstance(view)) {
return view;
}
Resources resources = view.getContext().getResources();
for (int index = 0; index < attrs.getAttributeCount(); index++) {
String attributeName = attrs.getAttributeName(index);
switch (attributeName) {
case ATTRIBUTE_APP_TITLE:
case ATTRIBUTE_TITLE: {
String value = attrs.getAttributeValue(index);
if (value != null && value.startsWith("@")) {
setTitleForView(view, resources.getString(attrs.getAttributeResourceValue(index, 0)));
}
break;
}
}
}
return view;
}
private void setTitleForView(View view, String text) {
((Toolbar) view).setTitle(text);
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/TextViewTransformer.java
================================================
package com.ice.restring;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* A transformer which transforms TextView(or any view extends it like Button, EditText, ...):
* it transforms "text" & "hint" attributes.
*/
class TextViewTransformer implements ViewTransformerManager.Transformer {
private static final String ATTRIBUTE_TEXT = "text";
private static final String ATTRIBUTE_ANDROID_TEXT = "android:text";
private static final String ATTRIBUTE_HINT = "hint";
private static final String ATTRIBUTE_ANDROID_HINT = "android:hint";
@Override
public Class extends View> getViewType() {
return TextView.class;
}
@Override
public View transform(View view, AttributeSet attrs) {
if (view == null || !getViewType().isInstance(view)) {
return view;
}
Resources resources = view.getContext().getResources();
for (int index = 0; index < attrs.getAttributeCount(); index++) {
String attributeName = attrs.getAttributeName(index);
switch (attributeName) {
case ATTRIBUTE_ANDROID_TEXT:
case ATTRIBUTE_TEXT: {
String value = attrs.getAttributeValue(index);
if (value != null && value.startsWith("@")) {
setTextForView(view, resources.getString(attrs.getAttributeResourceValue(index, 0)));
}
break;
}
case ATTRIBUTE_ANDROID_HINT:
case ATTRIBUTE_HINT: {
String value = attrs.getAttributeValue(index);
if (value != null && value.startsWith("@")) {
setHintForView(view, resources.getString(attrs.getAttributeResourceValue(index, 0)));
}
break;
}
}
}
return view;
}
private void setTextForView(View view, String text) {
((TextView) view).setText(text);
}
private void setHintForView(View view, String text) {
((TextView) view).setHint(text);
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/ToolbarTransformer.java
================================================
package com.ice.restring;
import android.annotation.TargetApi;
import android.content.res.Resources;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toolbar;
/**
* A transformer which transforms Toolbar: it transforms the text set as title.
*/
class ToolbarTransformer implements ViewTransformerManager.Transformer {
private static final String ATTRIBUTE_TITLE = "title";
private static final String ATTRIBUTE_ANDROID_TITLE = "android:title";
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Class extends View> getViewType() {
return Toolbar.class;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public View transform(View view, AttributeSet attrs) {
if (view == null || !getViewType().isInstance(view)) {
return view;
}
Resources resources = view.getContext().getResources();
for (int index = 0; index < attrs.getAttributeCount(); index++) {
String attributeName = attrs.getAttributeName(index);
switch (attributeName) {
case ATTRIBUTE_ANDROID_TITLE:
case ATTRIBUTE_TITLE: {
String value = attrs.getAttributeValue(index);
if (value != null && value.startsWith("@")) {
setTitleForView(view, resources.getString(attrs.getAttributeResourceValue(index, 0)));
}
break;
}
}
}
return view;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void setTitleForView(View view, String text) {
((Toolbar) view).setTitle(text);
}
}
================================================
FILE: restring/src/main/java/com/ice/restring/ViewTransformerManager.java
================================================
package com.ice.restring;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
* Manages all view transformers as a central point for layout inflater.
* Layout inflater will ask this manager to transform the inflating views.
*/
class ViewTransformerManager {
private List, Transformer>> transformers = new ArrayList<>();
/**
* Register a new view transformer to be applied on newly inflating views.
*
* @param transformer to be added to transformers list.
*/
void registerTransformer(Transformer transformer) {
transformers.add(new Pair<>(transformer.getViewType(), transformer));
}
/**
* Transforms a view.
* it tries to find proper transformers for view, and if exists, it will apply them on view,
* and return the final result as a new view.
*
* @param view to be transformed.
* @param attrs attributes of the view.
* @return the transformed view.
*/
View transform(View view, AttributeSet attrs) {
if (view == null) {
return null;
}
View newView = view;
for (Pair, Transformer> pair : transformers) {
Class extends View> type = pair.first;
if (!type.isInstance(view)) {
continue;
}
Transformer transformer = pair.second;
newView = transformer.transform(newView, attrs);
}
return newView;
}
/**
* A view transformer skeleton.
*/
interface Transformer {
/**
* The type of view this transformer is for.
*
* @return the type of view.
*/
Class extends View> getViewType();
/**
* Apply transformation to a view.
*
* @param view to be transformed.
* @param attrs attributes of the view.
* @return the transformed view.
*/
View transform(View view, AttributeSet attrs);
}
}
================================================
FILE: restring/src/main/res/layout/test_layout.xml
================================================
================================================
FILE: restring/src/main/res/menu/test_menu.xml
================================================
================================================
FILE: restring/src/main/res/values/strings.xml
================================================
headerhintMenu 1Menu1Menu 2Menu2Menu 3Menu3
================================================
FILE: restring/src/test/java/com/ice/restring/MemoryStringRepositoryTest.java
================================================
package com.ice.restring;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
@RunWith(JUnit4.class)
public class MemoryStringRepositoryTest {
private StringRepository stringRepository;
@Before
public void setUp() {
stringRepository = new MemoryStringRepository();
}
@Test
public void shouldSetAndGetStringPairs() {
final String LANGUAGE = "en";
Map strings = generateStrings(10);
stringRepository.setStrings(LANGUAGE, strings);
assertEquals(strings, stringRepository.getStrings(LANGUAGE));
}
@Test
public void shouldGetSingleString() {
final String LANGUAGE = "en";
final int STR_COUNT = 10;
Map strings = generateStrings(STR_COUNT);
stringRepository.setStrings(LANGUAGE, strings);
for (int i = 0; i < STR_COUNT; i++) {
assertEquals(stringRepository.getString(LANGUAGE, "key" + i), "value" + i);
}
}
@Test
public void shouldSetSingleString() {
final String LANGUAGE = "en";
final int STR_COUNT = 10;
Map strings = generateStrings(STR_COUNT);
stringRepository.setStrings(LANGUAGE, strings);
stringRepository.setString(LANGUAGE, "key5", "aNewValue");
assertEquals(stringRepository.getString(LANGUAGE, "key5"), "aNewValue");
}
private Map generateStrings(int count) {
Map strings = new LinkedHashMap<>();
for (int i = 0; i < count; i++) {
strings.put("key" + i, "value" + i);
}
return strings;
}
}
================================================
FILE: restring/src/test/java/com/ice/restring/RestringContextWrapperTest.java
================================================
package com.ice.restring;
import android.content.Context;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.ice.restring.shadow.MyShadowAssetManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {MyShadowAssetManager.class})
public class RestringContextWrapperTest {
private static final int STR_RES_ID = 0x7f0f0123;
private static final String STR_KEY = "STR_KEY";
private static final String STR_VALUE = "STR_VALUE";
private RestringContextWrapper restringContextWrapper;
private Context context;
private Resources originalResources;
@Mock private StringRepository stringRepository;
@Mock private ViewTransformerManager transformerManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
context = RuntimeEnvironment.application;
originalResources = context.getResources();
when(transformerManager.transform(any(), any())).thenAnswer(i -> i.getArgument(0));
restringContextWrapper = RestringContextWrapper.wrap(
context,
stringRepository,
transformerManager
);
}
@Test
public void shouldWrapResourcesAndGetStringsFromRepository() {
((MyShadowAssetManager) Shadow.extract(originalResources.getAssets()))
.addResourceEntryNameForTesting(STR_RES_ID, STR_KEY);
doReturn(STR_VALUE).when(stringRepository).getString(getLanguage(), STR_KEY);
String real = restringContextWrapper.getResources().getString(STR_RES_ID);
assertEquals(STR_VALUE, real);
}
@Test
public void shouldProvideCustomLayoutInflaterToApplyViewTransformation() {
LayoutInflater layoutInflater = (LayoutInflater) restringContextWrapper.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
assertTrue(layoutInflater instanceof RestringLayoutInflater);
ViewGroup viewGroup = (ViewGroup) layoutInflater.inflate(R.layout.test_layout, null, false);
ArgumentCaptor captor = ArgumentCaptor.forClass(View.class);
verify(transformerManager, atLeastOnce()).transform(captor.capture(), any());
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
captor.getAllValues().contains(child);
}
}
private String getLanguage() {
return Locale.getDefault().getLanguage();
}
}
================================================
FILE: restring/src/test/java/com/ice/restring/RestringLayoutInflaterTest.java
================================================
package com.ice.restring;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = {16, 19, 21, 23, 24, 26})
public class RestringLayoutInflaterTest {
@Mock private ViewTransformerManager transformerManager;
private RestringLayoutInflater restringLayoutInflater;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(transformerManager.transform(any(), any())).thenAnswer((Answer) invocation ->
invocation.getArgument(0)
);
RuntimeEnvironment.application.setTheme(R.style.Theme_AppCompat);
restringLayoutInflater = new RestringLayoutInflater(
LayoutInflater.from(RuntimeEnvironment.application),
RuntimeEnvironment.application,
transformerManager,
false
);
}
@Test
public void shouldTransformViewsOnInflatingLayouts() {
ViewGroup viewGroup = (ViewGroup) restringLayoutInflater.inflate(R.layout.test_layout, null, false);
ArgumentCaptor captor = ArgumentCaptor.forClass(View.class);
verify(transformerManager, atLeastOnce()).transform(captor.capture(), any());
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
captor.getAllValues().contains(child);
}
}
}
================================================
FILE: restring/src/test/java/com/ice/restring/RestringResourcesTest.java
================================================
package com.ice.restring;
import android.content.res.Resources;
import android.text.Html;
import android.text.TextUtils;
import com.ice.restring.shadow.MyShadowAssetManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {MyShadowAssetManager.class})
public class RestringResourcesTest {
private static final int STR_RES_ID = 0x7f0f0123;
private static final String STR_KEY = "STR_KEY";
private static final String STR_VALUE = "STR_VALUE";
private static final String STR_VALUE_WITH_PARAM = "STR_VALUE %s";
private static final String STR_VALUE_HTML = "STR_value";
@Mock private StringRepository repository;
private Resources resources;
private RestringResources restringResources;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
resources = RuntimeEnvironment.application.getResources();
restringResources = Mockito.spy(new RestringResources(resources, repository));
doReturn(STR_KEY).when(restringResources).getResourceEntryName(STR_RES_ID);
}
@Test
public void shouldGetStringFromRepositoryIfExists() {
doReturn(STR_VALUE).when(repository).getString(getLanguage(), STR_KEY);
String stringValue = restringResources.getString(STR_RES_ID);
assertEquals(STR_VALUE, stringValue);
}
@Test
public void shouldGetStringFromResourceIfNotExists() {
doReturn(null).when(repository).getString(getLanguage(), STR_KEY);
String stringValue = restringResources.getString(STR_RES_ID);
String expected = new MyShadowAssetManager().getResourceText(STR_RES_ID).toString();
assertEquals(expected, stringValue);
}
@Test
public void shouldGetStringWithParamsFromRepositoryIfExists() {
final String param = "PARAM";
doReturn(STR_VALUE_WITH_PARAM).when(repository).getString(getLanguage(), STR_KEY);
String stringValue = restringResources.getString(STR_RES_ID, param);
assertEquals(String.format(STR_VALUE_WITH_PARAM, param), stringValue);
}
@Test
public void shouldGetStringWithParamsFromResourceIfNotExists() {
final String param = "PARAM";
doReturn(null).when(repository).getString(getLanguage(), STR_KEY);
String stringValue = restringResources.getString(STR_RES_ID, param);
String expected = new MyShadowAssetManager().getResourceText(STR_RES_ID).toString();
assertEquals(expected, stringValue);
}
@Test
public void shouldGetHtmlTextFromRepositoryIfExists() {
doReturn(STR_VALUE_HTML).when(repository).getString(getLanguage(), STR_KEY);
CharSequence realValue = restringResources.getText(STR_RES_ID);
CharSequence expected = Html.fromHtml(STR_VALUE_HTML, Html.FROM_HTML_MODE_COMPACT);
assertTrue(TextUtils.equals(expected, realValue));
}
@Test
public void shouldGetHtmlTextFromResourceIfNotExists() {
doReturn(null).when(repository).getString(getLanguage(), STR_KEY);
CharSequence realValue = restringResources.getText(STR_RES_ID);
CharSequence expected = new MyShadowAssetManager().getResourceText(STR_RES_ID);
assertTrue(TextUtils.equals(expected, realValue));
}
@Test
public void shouldReturnDefaultHtmlTextFromRepositoryIfResourceIdIsInvalid() {
final CharSequence def = Html.fromHtml("def", Html.FROM_HTML_MODE_COMPACT);
doReturn(null).when(repository).getString(getLanguage(), STR_KEY);
CharSequence realValue = restringResources.getText(0, def);
assertTrue(TextUtils.equals(def, realValue));
}
private String getLanguage() {
return Locale.getDefault().getLanguage();
}
}
================================================
FILE: restring/src/test/java/com/ice/restring/RestringTest.java
================================================
package com.ice.restring;
import android.app.Activity;
import android.support.design.widget.BottomNavigationView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toolbar;
import com.ice.restring.activity.TestActivity;
import com.ice.restring.shadow.MyShadowAsyncTask;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static org.hamcrest.core.StringStartsWith.startsWith;
import static org.junit.Assert.assertThat;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {MyShadowAsyncTask.class})
public class RestringTest {
@Before
public void setUp() {
Restring.init(
RuntimeEnvironment.application,
new RestringConfig.Builder()
.persist(false)
.stringsLoader(new MyStringLoader())
.build()
);
Robolectric.flushBackgroundThreadScheduler();
}
@Test
public void shouldInflateAndTransformViewsOnActivityCreation() {
List languages = Arrays.asList("en", "fa", "de");
for (String lang : languages) {
Locale.setDefault(new Locale(lang));
ActivityController activityController = Robolectric.buildActivity(TestActivity.class);
Activity activity = activityController.create().start().resume().visible().get();
ViewGroup viewGroup = activity.findViewById(R.id.root_container);
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View view = viewGroup.getChildAt(i);
if (view instanceof TextView) {
assertThat("TextView[text]", ((TextView) view).getText().toString(), startsWith(getLanguage()));
assertThat("TextView[hint]", ((TextView) view).getHint().toString(), startsWith(getLanguage()));
} else if (view instanceof Toolbar) {
assertThat("Toolbar[title]", ((Toolbar) view).getTitle().toString(), startsWith(getLanguage()));
} else if (view instanceof android.support.v7.widget.Toolbar) {
assertThat("Toolbar[title]", ((android.support.v7.widget.Toolbar) view).getTitle().toString(), startsWith(getLanguage()));
} else if (view instanceof BottomNavigationView) {
BottomNavigationView bottomNavigationView = (BottomNavigationView) view;
int itemCount = bottomNavigationView.getMenu().size();
for (int item = 0; item < itemCount; item++) {
assertThat("BottomNavigationView#" + item + "[title]",
bottomNavigationView.getMenu().getItem(item).getTitle().toString(), startsWith(getLanguage()));
assertThat("BottomNavigationView#" + item + "[titleCondensed]",
bottomNavigationView.getMenu().getItem(item).getTitleCondensed().toString(), startsWith(getLanguage()));
}
}
}
activityController.pause().stop().destroy();
}
}
private String getLanguage() {
return Locale.getDefault().getLanguage();
}
private class MyStringLoader implements Restring.StringsLoader {
@Override
public List getLanguages() {
return Arrays.asList("en", "fa", "de");
}
@Override
public Map getStrings(String language) {
Map strings = new LinkedHashMap<>();
strings.put("header", language + "_" + "header");
strings.put("header_hint", language + "_" + "hint");
strings.put("menu1title", language + "_" + "Menu 1");
strings.put("menu1titleCondensed", language + "_" + "Menu1");
strings.put("menu2title", language + "_" + "Menu 2");
strings.put("menu2titleCondensed", language + "_" + "Menu2");
strings.put("menu3title", language + "_" + "Menu 3");
strings.put("menu3titleCondensed", language + "_" + "Menu3");
return strings;
}
}
}
================================================
FILE: restring/src/test/java/com/ice/restring/SharedPrefStringRepositoryTest.java
================================================
package com.ice.restring;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
public class SharedPrefStringRepositoryTest {
@Before
public void setUp() {
}
@Test
public void shouldSetAndGetStringPairs() {
final String LANGUAGE = "en";
Map strings = generateStrings(10);
StringRepository stringRepository = new SharedPrefStringRepository(RuntimeEnvironment.application);
stringRepository.setStrings(LANGUAGE, strings);
StringRepository newRepository = new SharedPrefStringRepository(RuntimeEnvironment.application);
assertEquals(strings, newRepository.getStrings(LANGUAGE));
}
@Test
public void shouldGetSingleString() {
final String LANGUAGE = "en";
final int STR_COUNT = 10;
Map strings = generateStrings(STR_COUNT);
StringRepository stringRepository = new SharedPrefStringRepository(RuntimeEnvironment.application);
stringRepository.setStrings(LANGUAGE, strings);
StringRepository newRepository = new SharedPrefStringRepository(RuntimeEnvironment.application);
for (int i = 0; i < STR_COUNT; i++) {
assertEquals(newRepository.getString(LANGUAGE, "key" + i), "value" + i);
}
}
@Test
public void shouldSetSingleString() {
final String LANGUAGE = "en";
final int STR_COUNT = 10;
Map strings = generateStrings(STR_COUNT);
StringRepository stringRepository = new SharedPrefStringRepository(RuntimeEnvironment.application);
stringRepository.setStrings(LANGUAGE, strings);
stringRepository.setString(LANGUAGE, "key5", "aNewValue");
StringRepository newRepository = new SharedPrefStringRepository(RuntimeEnvironment.application);
assertEquals(newRepository.getString(LANGUAGE, "key5"), "aNewValue");
}
private Map generateStrings(int count) {
Map strings = new LinkedHashMap<>();
for (int i = 0; i < count; i++) {
strings.put("key" + i, "value" + i);
}
return strings;
}
}
================================================
FILE: restring/src/test/java/com/ice/restring/StringsLoaderTaskTest.java
================================================
package com.ice.restring;
import com.ice.restring.shadow.MyShadowAsyncTask;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {MyShadowAsyncTask.class})
public class StringsLoaderTaskTest {
@Before
public void setUp() {
}
@Test
public void shouldLoadStringsAndSaveInRepository() {
List langs = Arrays.asList("en", "fa");
Map enStrings = new HashMap<>();
enStrings.put("string1", "value1");
enStrings.put("string2", "value2");
Map deStrings = new HashMap<>();
deStrings.put("string3", "value3");
deStrings.put("string4", "value4");
Restring.StringsLoader loader = Mockito.mock(Restring.StringsLoader.class);
when(loader.getLanguages()).thenReturn(langs);
when(loader.getStrings("en")).thenReturn(enStrings);
when(loader.getStrings("fa")).thenReturn(deStrings);
StringRepository repository = Mockito.mock(StringRepository.class);
StringsLoaderTask task = new StringsLoaderTask(loader, repository);
task.run();
Robolectric.flushBackgroundThreadScheduler();
Robolectric.flushForegroundThreadScheduler();
ArgumentCaptor