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 ================================================ [![](https://img.shields.io/badge/AndroidWeekly-%23307-yellow.svg)](http://androidweekly.net/issues/issue-307) Android Arsenal ### 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.
================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.2.0-alpha06' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0' classpath 'org.jacoco:org.jacoco.core:0.8.0' } } allprojects { repositories { google() jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } ext { compileSdkVersion = 27 buildToolsVersion = '27.0.3' minSdkVersion = 16 targetSdkVersion = 27 //Library versions supportLibrariesVersion = '27.1.0' deps = [ supportAppCompat: "com.android.support:appcompat-v7:$supportLibrariesVersion", supportDesign : "com.android.support:design:$supportLibrariesVersion", constraintLayout: "com.android.support.constraint:constraint-layout:1.0.2", junit : 'junit:junit:4.12', hamcrest : 'org.hamcrest:hamcrest-junit:2.0.0.0', mockito : 'org.mockito:mockito-core:2.+', robolectric : 'org.robolectric:robolectric:3.7.1' ] } ================================================ FILE: docs/META-INF/MANIFEST.MF ================================================ Manifest-Version: 1.0 ================================================ FILE: docs/allclasses-frame.html ================================================ All Classes (restring 1.0.0 API)

All Classes

================================================ FILE: docs/allclasses-noframe.html ================================================ All Classes (restring 1.0.0 API)

All Classes

================================================ FILE: docs/com/ice/restring/Restring.StringsLoader.html ================================================ Restring.StringsLoader (restring 1.0.0 API)
com.ice.restring

Interface Restring.StringsLoader

  • Enclosing class:
    Restring


    public static interface Restring.StringsLoader
    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.
    • Method Summary

      All Methods Instance Methods Abstract Methods 
      Modifier and Type Method and Description
      java.util.List<java.lang.String> getLanguages()
      Get supported languages.
      java.util.Map<java.lang.String,java.lang.String> getStrings(java.lang.String language)
      Get strings of a language as keys & values.
    • Method Detail

      • getLanguages

        java.util.List<java.lang.String> getLanguages()
        Get supported languages.
        Returns:
        the list of languages.
      • getStrings

        java.util.Map<java.lang.String,java.lang.String> getStrings(java.lang.String language)
        Get strings of a language as keys & values.
        Parameters:
        language - of the strings.
        Returns:
        the strings as (key, value).
================================================ FILE: docs/com/ice/restring/Restring.html ================================================ Restring (restring 1.0.0 API)
com.ice.restring

Class Restring

  • java.lang.Object
    • com.ice.restring.Restring


  • public abstract class Restring
    extends java.lang.Object
    Entry point for Restring. it will be used for initializing Restring components, setting new strings, wrapping activity context.
    • Nested Class Summary

      Nested Classes 
      Modifier and Type Class and Description
      static interface  Restring.StringsLoader
      Loader of strings skeleton.
    • Constructor Summary

      Constructors 
      Constructor and Description
      Restring() 
    • Method Summary

      All Methods Static Methods Concrete Methods 
      Modifier and Type Method and Description
      static void init(android.content.Context context)
      Initialize Restring with default configuration.
      static void init(android.content.Context context, RestringConfig config)
      Initialize Restring with the specified configuration.
      static void setString(java.lang.String language, java.lang.String key, java.lang.String value)
      Set a single string for a language.
      static void setStrings(java.lang.String language, java.util.Map<java.lang.String,java.lang.String> newStrings)
      Set strings of a language.
      static android.content.ContextWrapper wrapContext(android.content.Context base)
      Wraps context of an activity to provide Restring features.
      • Methods inherited from class java.lang.Object

        clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
    • Constructor Detail

      • Restring

        public Restring()
    • Method Detail

      • init

        public static void init(android.content.Context context)
        Initialize Restring with default configuration.
        Parameters:
        context - of the application.
      • init

        public static void init(android.content.Context context,
                                RestringConfig config)
        Initialize Restring with the specified configuration.
        Parameters:
        context - of the application.
        config - of the Restring.
      • wrapContext

        public static android.content.ContextWrapper wrapContext(android.content.Context base)
        Wraps context of an activity to provide Restring features.
        Parameters:
        base - context of an activity.
        Returns:
        the Restring wrapped context.
      • setStrings

        public static void setStrings(java.lang.String language,
                                      java.util.Map<java.lang.String,java.lang.String> newStrings)
        Set strings of a language.
        Parameters:
        language - the strings are for.
        newStrings - the strings of the language.
      • setString

        public static void setString(java.lang.String language,
                                     java.lang.String key,
                                     java.lang.String value)
        Set a single string for a language.
        Parameters:
        language - the string is for.
        key - the string key.
        value - the string value.
================================================ FILE: docs/com/ice/restring/RestringConfig.Builder.html ================================================ RestringConfig.Builder (restring 1.0.0 API)
com.ice.restring

Class RestringConfig.Builder

  • java.lang.Object
    • com.ice.restring.RestringConfig.Builder
  • Enclosing class:
    RestringConfig


    public static class RestringConfig.Builder
    extends java.lang.Object
================================================ FILE: docs/com/ice/restring/RestringConfig.html ================================================ RestringConfig (restring 1.0.0 API)
com.ice.restring

Class RestringConfig

  • java.lang.Object
    • com.ice.restring.RestringConfig


  • public class RestringConfig
    extends java.lang.Object
    Contains configuration properties for initializing Restring.
================================================ FILE: docs/com/ice/restring/package-frame.html ================================================ com.ice.restring (restring 1.0.0 API)

com.ice.restring

================================================ FILE: docs/com/ice/restring/package-summary.html ================================================ com.ice.restring (restring 1.0.0 API)

Package com.ice.restring

================================================ FILE: docs/com/ice/restring/package-tree.html ================================================ com.ice.restring Class Hierarchy (restring 1.0.0 API)

Hierarchy For Package com.ice.restring

Class Hierarchy

Interface Hierarchy

================================================ FILE: docs/constant-values.html ================================================ Constant Field Values (restring 1.0.0 API)

Constant Field Values

Contents

================================================ FILE: docs/deprecated-list.html ================================================ Deprecated List (restring 1.0.0 API)

Deprecated API

Contents

================================================ FILE: docs/help-doc.html ================================================ API Help (restring 1.0.0 API)

How This API Document Is Organized

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.

  • Constant Field Values

    The Constant Field Values page lists the static final fields and their values.

This help file applies to API documentation generated using the standard doclet.
================================================ FILE: docs/index-all.html ================================================ Index (restring 1.0.0 API)
B C G I P R S W 

B

build() - Method in class com.ice.restring.RestringConfig.Builder
 
Builder() - Constructor for class com.ice.restring.RestringConfig.Builder
 

C

com.ice.restring - package com.ice.restring
 

G

getLanguages() - Method in interface com.ice.restring.Restring.StringsLoader
Get supported languages.
getStrings(String) - Method in interface com.ice.restring.Restring.StringsLoader
Get strings of a language as keys & values.
getStringsLoader() - Method in class com.ice.restring.RestringConfig
 

I

init(Context) - Static method in class com.ice.restring.Restring
Initialize Restring with default configuration.
init(Context, RestringConfig) - Static method in class com.ice.restring.Restring
Initialize Restring with the specified configuration.
isPersist() - Method in class com.ice.restring.RestringConfig
 

P

persist(boolean) - Method in class com.ice.restring.RestringConfig.Builder
 

R

Restring - Class in com.ice.restring
Entry point for Restring.
Restring() - Constructor for class com.ice.restring.Restring
 
Restring.StringsLoader - Interface in com.ice.restring
Loader of strings skeleton.
RestringConfig - Class in com.ice.restring
Contains configuration properties for initializing Restring.
RestringConfig.Builder - Class in com.ice.restring
 

S

setString(String, String, String) - Static method in class com.ice.restring.Restring
Set a single string for a language.
setStrings(String, Map<String, String>) - Static method in class com.ice.restring.Restring
Set strings of a language.
stringsLoader(Restring.StringsLoader) - Method in class com.ice.restring.RestringConfig.Builder
 

W

wrapContext(Context) - Static method in class com.ice.restring.Restring
Wraps context of an activity to provide Restring features.
B C G I P R S W 
================================================ FILE: docs/index.html ================================================ restring 1.0.0 API <noscript> <div>JavaScript is disabled on your browser.</div> </noscript> <h2>Frame Alert</h2> <p>This document is designed to be viewed using the frames feature. If you see this message, you are using a non-frame-capable web client. Link to <a href="com/ice/restring/package-summary.html">Non-frame version</a>.</p> ================================================ FILE: docs/overview-tree.html ================================================ Class Hierarchy (restring 1.0.0 API)

Hierarchy For All Packages

Package Hierarchies:

Class Hierarchy

Interface Hierarchy

================================================ FILE: docs/package-list ================================================ com.ice.restring ================================================ FILE: docs/script.js ================================================ function show(type) { count = 0; for (var key in methods) { var row = document.getElementById(key); if ((methods[key] & type) != 0) { row.style.display = ''; row.className = (count++ % 2) ? rowColor : altColor; } else row.style.display = 'none'; } updateTabs(type); } function updateTabs(type) { for (var value in tabs) { var sNode = document.getElementById(tabs[value][0]); var spanNode = sNode.firstChild; if (value == type) { sNode.className = activeTableTab; spanNode.innerHTML = tabs[value][1]; } else { sNode.className = tableTab; spanNode.innerHTML = "" + tabs[value][1] + ""; } } } ================================================ FILE: docs/stylesheet.css ================================================ /* Javadoc style sheet */ /* Overall document style */ @import url('resources/fonts/dejavu.css'); body { background-color:#ffffff; color:#353833; font-family:'DejaVu Sans', Arial, Helvetica, sans-serif; font-size:14px; margin:0; } a:link, a:visited { text-decoration:none; color:#4A6782; } a:hover, a:focus { text-decoration:none; color:#bb7a2a; } a:active { text-decoration:none; color:#4A6782; } a[name] { color:#353833; } a[name]:hover { text-decoration:none; color:#353833; } pre { font-family:'DejaVu Sans Mono', monospace; font-size:14px; } h1 { font-size:20px; } h2 { font-size:18px; } h3 { font-size:16px; font-style:italic; } h4 { font-size:13px; } h5 { font-size:12px; } h6 { font-size:11px; } ul { list-style-type:disc; } code, tt { font-family:'DejaVu Sans Mono', monospace; font-size:14px; padding-top:4px; margin-top:8px; line-height:1.4em; } dt code { font-family:'DejaVu Sans Mono', monospace; font-size:14px; padding-top:4px; } table tr td dt code { font-family:'DejaVu Sans Mono', monospace; font-size:14px; vertical-align:top; padding-top:4px; } sup { font-size:8px; } /* Document title and Copyright styles */ .clear { clear:both; height:0px; overflow:hidden; } .aboutLanguage { float:right; padding:0px 21px; font-size:11px; z-index:200; margin-top:-9px; } .legalCopy { margin-left:.5em; } .bar a, .bar a:link, .bar a:visited, .bar a:active { color:#FFFFFF; text-decoration:none; } .bar a:hover, .bar a:focus { color:#bb7a2a; } .tab { background-color:#0066FF; color:#ffffff; padding:8px; width:5em; font-weight:bold; } /* Navigation bar styles */ .bar { background-color:#4D7A97; color:#FFFFFF; padding:.8em .5em .4em .8em; height:auto;/*height:1.8em;*/ font-size:11px; margin:0; } .topNav { background-color:#4D7A97; color:#FFFFFF; float:left; padding:0; width:100%; clear:right; height:2.8em; padding-top:10px; overflow:hidden; font-size:12px; } .bottomNav { margin-top:10px; background-color:#4D7A97; color:#FFFFFF; float:left; padding:0; width:100%; clear:right; height:2.8em; padding-top:10px; overflow:hidden; font-size:12px; } .subNav { background-color:#dee3e9; float:left; width:100%; overflow:hidden; font-size:12px; } .subNav div { clear:left; float:left; padding:0 0 5px 6px; text-transform:uppercase; } ul.navList, ul.subNavList { float:left; margin:0 25px 0 0; padding:0; } ul.navList li{ list-style:none; float:left; padding: 5px 6px; text-transform:uppercase; } ul.subNavList li{ list-style:none; float:left; } .topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited { color:#FFFFFF; text-decoration:none; text-transform:uppercase; } .topNav a:hover, .bottomNav a:hover { text-decoration:none; color:#bb7a2a; text-transform:uppercase; } .navBarCell1Rev { background-color:#F8981D; color:#253441; margin: auto 5px; } .skipNav { position:absolute; top:auto; left:-9999px; overflow:hidden; } /* Page header and footer styles */ .header, .footer { clear:both; margin:0 20px; padding:5px 0 0 0; } .indexHeader { margin:10px; position:relative; } .indexHeader span{ margin-right:15px; } .indexHeader h1 { font-size:13px; } .title { color:#2c4557; margin:10px 0; } .subTitle { margin:5px 0 0 0; } .header ul { margin:0 0 15px 0; padding:0; } .footer ul { margin:20px 0 5px 0; } .header ul li, .footer ul li { list-style:none; font-size:13px; } /* Heading styles */ div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 { background-color:#dee3e9; border:1px solid #d0d9e0; margin:0 0 6px -8px; padding:7px 5px; } ul.blockList ul.blockList ul.blockList li.blockList h3 { background-color:#dee3e9; border:1px solid #d0d9e0; margin:0 0 6px -8px; padding:7px 5px; } ul.blockList ul.blockList li.blockList h3 { padding:0; margin:15px 0; } ul.blockList li.blockList h2 { padding:0px 0 20px 0; } /* Page layout container styles */ .contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer { clear:both; padding:10px 20px; position:relative; } .indexContainer { margin:10px; position:relative; font-size:12px; } .indexContainer h2 { font-size:13px; padding:0 0 3px 0; } .indexContainer ul { margin:0; padding:0; } .indexContainer ul li { list-style:none; padding-top:2px; } .contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt { font-size:12px; font-weight:bold; margin:10px 0 0 0; color:#4E4E4E; } .contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { margin:5px 0 10px 0px; font-size:14px; font-family:'DejaVu Sans Mono',monospace; } .serializedFormContainer dl.nameValue dt { margin-left:1px; font-size:1.1em; display:inline; font-weight:bold; } .serializedFormContainer dl.nameValue dd { margin:0 0 0 1px; font-size:1.1em; display:inline; } /* List styles */ ul.horizontal li { display:inline; font-size:0.9em; } ul.inheritance { margin:0; padding:0; } ul.inheritance li { display:inline; list-style:none; } ul.inheritance li ul.inheritance { margin-left:15px; padding-left:15px; padding-top:1px; } ul.blockList, ul.blockListLast { margin:10px 0 10px 0; padding:0; } ul.blockList li.blockList, ul.blockListLast li.blockList { list-style:none; margin-bottom:15px; line-height:1.4; } ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList { padding:0px 20px 5px 10px; border:1px solid #ededed; background-color:#f8f8f8; } ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList { padding:0 0 5px 8px; background-color:#ffffff; border:none; } ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { margin-left:0; padding-left:0; padding-bottom:15px; border:none; } ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { list-style:none; border-bottom:none; padding-bottom:0; } table tr td dl, table tr td dl dt, table tr td dl dd { margin-top:0; margin-bottom:1px; } /* Table styles */ .overviewSummary, .memberSummary, .typeSummary, .useSummary, .constantsSummary, .deprecatedSummary { width:100%; border-left:1px solid #EEE; border-right:1px solid #EEE; border-bottom:1px solid #EEE; } .overviewSummary, .memberSummary { padding:0px; } .overviewSummary caption, .memberSummary caption, .typeSummary caption, .useSummary caption, .constantsSummary caption, .deprecatedSummary caption { position:relative; text-align:left; background-repeat:no-repeat; color:#253441; font-weight:bold; clear:none; overflow:hidden; padding:0px; padding-top:10px; padding-left:1px; margin:0px; white-space:pre; } .overviewSummary caption a:link, .memberSummary caption a:link, .typeSummary caption a:link, .useSummary caption a:link, .constantsSummary caption a:link, .deprecatedSummary caption a:link, .overviewSummary caption a:hover, .memberSummary caption a:hover, .typeSummary caption a:hover, .useSummary caption a:hover, .constantsSummary caption a:hover, .deprecatedSummary caption a:hover, .overviewSummary caption a:active, .memberSummary caption a:active, .typeSummary caption a:active, .useSummary caption a:active, .constantsSummary caption a:active, .deprecatedSummary caption a:active, .overviewSummary caption a:visited, .memberSummary caption a:visited, .typeSummary caption a:visited, .useSummary caption a:visited, .constantsSummary caption a:visited, .deprecatedSummary caption a:visited { color:#FFFFFF; } .overviewSummary caption span, .memberSummary caption span, .typeSummary caption span, .useSummary caption span, .constantsSummary caption span, .deprecatedSummary caption span { white-space:nowrap; padding-top:5px; padding-left:12px; padding-right:12px; padding-bottom:7px; display:inline-block; float:left; background-color:#F8981D; border: none; height:16px; } .memberSummary caption span.activeTableTab span { white-space:nowrap; padding-top:5px; padding-left:12px; padding-right:12px; margin-right:3px; display:inline-block; float:left; background-color:#F8981D; height:16px; } .memberSummary caption span.tableTab span { white-space:nowrap; padding-top:5px; padding-left:12px; padding-right:12px; margin-right:3px; display:inline-block; float:left; background-color:#4D7A97; height:16px; } .memberSummary caption span.tableTab, .memberSummary caption span.activeTableTab { padding-top:0px; padding-left:0px; padding-right:0px; background-image:none; float:none; display:inline; } .overviewSummary .tabEnd, .memberSummary .tabEnd, .typeSummary .tabEnd, .useSummary .tabEnd, .constantsSummary .tabEnd, .deprecatedSummary .tabEnd { display:none; width:5px; position:relative; float:left; background-color:#F8981D; } .memberSummary .activeTableTab .tabEnd { display:none; width:5px; margin-right:3px; position:relative; float:left; background-color:#F8981D; } .memberSummary .tableTab .tabEnd { display:none; width:5px; margin-right:3px; position:relative; background-color:#4D7A97; float:left; } .overviewSummary td, .memberSummary td, .typeSummary td, .useSummary td, .constantsSummary td, .deprecatedSummary td { text-align:left; padding:0px 0px 12px 10px; } th.colOne, th.colFirst, th.colLast, .useSummary th, .constantsSummary th, td.colOne, td.colFirst, td.colLast, .useSummary td, .constantsSummary td{ vertical-align:top; padding-right:0px; padding-top:8px; padding-bottom:3px; } th.colFirst, th.colLast, th.colOne, .constantsSummary th { background:#dee3e9; text-align:left; padding:8px 3px 3px 7px; } td.colFirst, th.colFirst { white-space:nowrap; font-size:13px; } td.colLast, th.colLast { font-size:13px; } td.colOne, th.colOne { font-size:13px; } .overviewSummary td.colFirst, .overviewSummary th.colFirst, .useSummary td.colFirst, .useSummary th.colFirst, .overviewSummary td.colOne, .overviewSummary th.colOne, .memberSummary td.colFirst, .memberSummary th.colFirst, .memberSummary td.colOne, .memberSummary th.colOne, .typeSummary td.colFirst{ width:25%; vertical-align:top; } td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover { font-weight:bold; } .tableSubHeadingColor { background-color:#EEEEFF; } .altColor { background-color:#FFFFFF; } .rowColor { background-color:#EEEEEF; } /* Content styles */ .description pre { margin-top:0; } .deprecatedContent { margin:0; padding:10px 0; } .docSummary { padding:0; } ul.blockList ul.blockList ul.blockList li.blockList h3 { font-style:normal; } div.block { font-size:14px; font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; } td.colLast div { padding-top:0px; } td.colLast a { padding-bottom:3px; } /* Formatting effect styles */ .sourceLineNo { color:green; padding:0 30px 0 0; } h1.hidden { visibility:hidden; overflow:hidden; font-size:10px; } .block { display:block; margin:3px 10px 2px 0px; color:#474747; } .deprecatedLabel, .descfrmTypeLabel, .memberNameLabel, .memberNameLink, .overrideSpecifyLabel, .packageHierarchyLabel, .paramLabel, .returnLabel, .seeLabel, .simpleTagLabel, .throwsLabel, .typeNameLabel, .typeNameLink { font-weight:bold; } .deprecationComment, .emphasizedPhrase, .interfaceName { font-style:italic; } div.block div.block span.deprecationComment, div.block div.block span.emphasizedPhrase, div.block div.block span.interfaceName { font-style:normal; } div.contentContainer ul.blockList li.blockList h2{ padding-bottom:0px; } ================================================ FILE: example/.gitignore ================================================ /build ================================================ FILE: example/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { applicationId "com.ice.restring.example" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation project(":restring") implementation deps.supportAppCompat implementation deps.constraintLayout } ================================================ FILE: example/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: example/src/main/AndroidManifest.xml ================================================ ================================================ FILE: example/src/main/java/com/ice/restring/example/BaseActivity.java ================================================ package com.ice.restring.example; import android.content.Context; import android.support.v7.app.AppCompatActivity; import com.ice.restring.Restring; /** * We should wrap the base context of our activities, which is better to put it * on BaseActivity, so that we don't have to repeat it for all activities one-by-one. */ public class BaseActivity extends AppCompatActivity { @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(Restring.wrapContext(newBase)); } } ================================================ FILE: example/src/main/java/com/ice/restring/example/MainActivity.java ================================================ package com.ice.restring.example; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((TextView) findViewById(R.id.text_view2)).setText(R.string.subtitle); } } ================================================ FILE: example/src/main/java/com/ice/restring/example/SampleApplication.java ================================================ package com.ice.restring.example; import android.app.Application; import com.ice.restring.Restring; import com.ice.restring.RestringConfig; public class SampleApplication extends Application { @Override public void onCreate() { super.onCreate(); Restring.init(this, new RestringConfig.Builder() .persist(true) .stringsLoader(new SampleStringsLoader()) .build() ); } } ================================================ FILE: example/src/main/java/com/ice/restring/example/SampleStringsLoader.java ================================================ package com.ice.restring.example; import com.ice.restring.Restring; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * This is just a really simple sample of strings loader. * in real applications, you might call an API to get your strings. *

* 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 ================================================ Restring This 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 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 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 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 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 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 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 ================================================ header hint Menu 1 Menu1 Menu 2 Menu2 Menu 3 Menu3 ================================================ 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> enCaptor = ArgumentCaptor.forClass(Map.class); verify(repository).setStrings(eq("en"), enCaptor.capture()); assertEquals(enStrings, enCaptor.getValue()); ArgumentCaptor> deCaptor = ArgumentCaptor.forClass(Map.class); verify(repository).setStrings(eq("fa"), deCaptor.capture()); assertEquals(deStrings, deCaptor.getValue()); } } ================================================ FILE: restring/src/test/java/com/ice/restring/SupportToolbarTransformerTest.java ================================================ package com.ice.restring; import android.content.Context; import android.content.res.Resources; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.util.AttributeSet; import android.view.View; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) public class SupportToolbarTransformerTest { private static final int TITLE_ATTR_INDEX = 3; private static final int TITLE_RES_ID = 0x7f0f0123; private static final String TITLE_ATTR_KEY = "title"; private static final String TITLE_ATTR_VALUE = "TITLE_ATTR_VALUE"; private SupportToolbarTransformer transformer; @Before public void setUp() { transformer = new SupportToolbarTransformer(); } @Test public void shouldTransformToolbar() { Context context = getContext(); View view = transformer.transform(new Toolbar(context), getAttributeSet(false)); assertTrue(view instanceof Toolbar); assertEquals(((Toolbar) view).getTitle(), TITLE_ATTR_VALUE); view = transformer.transform(new Toolbar(context), getAttributeSet(true)); assertTrue(view instanceof Toolbar); assertEquals(((Toolbar) view).getTitle(), TITLE_ATTR_VALUE); } @Test public void shouldRejectOtherViewTypes() { Context context = getContext(); AttributeSet attributeSet = getAttributeSet(false); RecyclerView recyclerView = new RecyclerView(context); View view = transformer.transform(recyclerView, attributeSet); assertSame(view, recyclerView); verifyZeroInteractions(attributeSet); } private Context getContext() { Context context = Mockito.spy(RuntimeEnvironment.application); Resources resources = Mockito.spy(context.getResources()); doReturn(resources).when(context).getResources(); doReturn(TITLE_ATTR_VALUE).when(resources).getString(TITLE_RES_ID); return context; } private AttributeSet getAttributeSet(boolean withAppPrefix) { AttributeSet attributeSet = Mockito.mock(AttributeSet.class); when(attributeSet.getAttributeCount()).thenReturn(TITLE_ATTR_INDEX + 2); when(attributeSet.getAttributeName(anyInt())).thenReturn("other_attribute"); when(attributeSet.getAttributeName(TITLE_ATTR_INDEX)).thenReturn((withAppPrefix ? "app:" : "") + TITLE_ATTR_KEY); when(attributeSet.getAttributeValue(TITLE_ATTR_INDEX)).thenReturn("@" + TITLE_RES_ID); when(attributeSet.getAttributeResourceValue(eq(TITLE_ATTR_INDEX), anyInt())).thenReturn(TITLE_RES_ID); return attributeSet; } } ================================================ FILE: restring/src/test/java/com/ice/restring/TextViewTransformerTest.java ================================================ package com.ice.restring; import android.content.Context; import android.content.res.Resources; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; import android.widget.EditText; import android.widget.TextView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) public class TextViewTransformerTest { private static final int TEXT_ATTR_INDEX = 3; private static final int TEXT_RES_ID = 0x7f0f0123; private static final String TEXT_ATTR_KEY = "text"; private static final String TEXT_ATTR_VALUE = "TEXT_ATTR_VALUE"; private static final int HINT_ATTR_INDEX = 2; private static final int HINT_RES_ID = 0x7f0f0124; private static final String HINT_ATTR_KEY = "hint"; private static final String HINT_ATTR_VALUE = "HINT_ATTR_VALUE"; private TextViewTransformer transformer; @Before public void setUp() { transformer = new TextViewTransformer(); } @Test public void shouldTransformTextView() { Context context = getContext(); View view = transformer.transform(new TextView(context), getAttributeSet(false)); assertTrue(view instanceof TextView); assertEquals(((TextView) view).getText(), TEXT_ATTR_VALUE); assertEquals(((TextView) view).getHint(), HINT_ATTR_VALUE); view = transformer.transform(new TextView(context), getAttributeSet(true)); assertTrue(view instanceof TextView); assertEquals(((TextView) view).getText(), TEXT_ATTR_VALUE); assertEquals(((TextView) view).getHint(), HINT_ATTR_VALUE); } @Test public void shouldTransformExtendedViews() { Context context = getContext(); View view = transformer.transform(new EditText(context), getAttributeSet(false)); assertTrue(view instanceof EditText); assertEquals(((EditText) view).getText().toString(), TEXT_ATTR_VALUE); assertEquals(((EditText) view).getHint(), HINT_ATTR_VALUE); view = transformer.transform(new EditText(context), getAttributeSet(true)); assertTrue(view instanceof EditText); assertEquals(((EditText) view).getText().toString(), TEXT_ATTR_VALUE); assertEquals(((EditText) view).getHint(), HINT_ATTR_VALUE); } @Test public void shouldRejectOtherViewTypes() { Context context = getContext(); AttributeSet attributeSet = getAttributeSet(false); RecyclerView recyclerView = new RecyclerView(context); View view = transformer.transform(recyclerView, attributeSet); assertSame(view, recyclerView); verifyZeroInteractions(attributeSet); } private Context getContext() { Context context = Mockito.spy(RuntimeEnvironment.application); Resources resources = Mockito.spy(context.getResources()); doReturn(resources).when(context).getResources(); doReturn(TEXT_ATTR_VALUE).when(resources).getString(TEXT_RES_ID); doReturn(HINT_ATTR_VALUE).when(resources).getString(HINT_RES_ID); return context; } private AttributeSet getAttributeSet(boolean withAndroidPrefix) { AttributeSet attributeSet = Mockito.mock(AttributeSet.class); when(attributeSet.getAttributeCount()).thenReturn(TEXT_ATTR_INDEX + 2); when(attributeSet.getAttributeName(anyInt())).thenReturn("other_attribute"); when(attributeSet.getAttributeName(TEXT_ATTR_INDEX)).thenReturn((withAndroidPrefix ? "android:" : "") + TEXT_ATTR_KEY); when(attributeSet.getAttributeValue(TEXT_ATTR_INDEX)).thenReturn("@" + TEXT_RES_ID); when(attributeSet.getAttributeResourceValue(eq(TEXT_ATTR_INDEX), anyInt())).thenReturn(TEXT_RES_ID); when(attributeSet.getAttributeName(HINT_ATTR_INDEX)).thenReturn((withAndroidPrefix ? "android:" : "") + HINT_ATTR_KEY); when(attributeSet.getAttributeValue(HINT_ATTR_INDEX)).thenReturn("@" + HINT_RES_ID); when(attributeSet.getAttributeResourceValue(eq(HINT_ATTR_INDEX), anyInt())).thenReturn(HINT_RES_ID); return attributeSet; } } ================================================ FILE: restring/src/test/java/com/ice/restring/ToolbarTransformerTest.java ================================================ package com.ice.restring; import android.content.Context; import android.content.res.Resources; import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.View; import android.widget.Toolbar; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) public class ToolbarTransformerTest { private static final int TITLE_ATTR_INDEX = 3; private static final int TITLE_RES_ID = 0x7f0f0123; private static final String TITLE_ATTR_KEY = "title"; private static final String TITLE_ATTR_VALUE = "TITLE_ATTR_VALUE"; private ToolbarTransformer transformer; @Before public void setUp() { transformer = new ToolbarTransformer(); } @Test public void shouldTransformToolbar() { Context context = getContext(); View view = transformer.transform(new Toolbar(context), getAttributeSet(false)); assertTrue(view instanceof Toolbar); assertEquals(((Toolbar) view).getTitle(), TITLE_ATTR_VALUE); view = transformer.transform(new Toolbar(context), getAttributeSet(true)); assertTrue(view instanceof Toolbar); assertEquals(((Toolbar) view).getTitle(), TITLE_ATTR_VALUE); } @Test public void shouldRejectOtherViewTypes() { Context context = getContext(); AttributeSet attributeSet = getAttributeSet(false); RecyclerView recyclerView = new RecyclerView(context); View view = transformer.transform(recyclerView, attributeSet); assertSame(view, recyclerView); verifyZeroInteractions(attributeSet); } private Context getContext() { Context context = Mockito.spy(RuntimeEnvironment.application); Resources resources = Mockito.spy(context.getResources()); doReturn(resources).when(context).getResources(); doReturn(TITLE_ATTR_VALUE).when(resources).getString(TITLE_RES_ID); return context; } private AttributeSet getAttributeSet(boolean withAndroidPrefix) { AttributeSet attributeSet = Mockito.mock(AttributeSet.class); when(attributeSet.getAttributeCount()).thenReturn(TITLE_ATTR_INDEX + 2); when(attributeSet.getAttributeName(anyInt())).thenReturn("other_attribute"); when(attributeSet.getAttributeName(TITLE_ATTR_INDEX)).thenReturn((withAndroidPrefix ? "android:" : "") + TITLE_ATTR_KEY); when(attributeSet.getAttributeValue(TITLE_ATTR_INDEX)).thenReturn("@" + TITLE_RES_ID); when(attributeSet.getAttributeResourceValue(eq(TITLE_ATTR_INDEX), anyInt())).thenReturn(TITLE_RES_ID); return attributeSet; } } ================================================ FILE: restring/src/test/java/com/ice/restring/ViewTransformerManagerTest.java ================================================ package com.ice.restring; import android.util.AttributeSet; import android.view.View; import android.widget.TextView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) public class ViewTransformerManagerTest { private ViewTransformerManager transformerManager; @Before public void setUp() { transformerManager = new ViewTransformerManager(); } @Test public void shouldTransformView() { TextView textView = new TextView(RuntimeEnvironment.application); ViewTransformerManager.Transformer transformer = Mockito.mock(ViewTransformerManager.Transformer.class); doReturn(TextView.class).when(transformer).getViewType(); when(transformer.transform(any(), any())).thenReturn(textView); transformerManager.registerTransformer(transformer); View transformedView = transformerManager.transform( new TextView(RuntimeEnvironment.application), Mockito.mock(AttributeSet.class) ); assertSame(textView, transformedView); } } ================================================ FILE: restring/src/test/java/com/ice/restring/activity/TestActivity.java ================================================ package com.ice.restring.activity; import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import com.ice.restring.R; import com.ice.restring.Restring; public class TestActivity extends AppCompatActivity { @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(Restring.wrapContext(newBase)); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTheme(R.style.Theme_AppCompat); setContentView(R.layout.test_layout); } } ================================================ FILE: restring/src/test/java/com/ice/restring/shadow/MyShadowAssetManager.java ================================================ package com.ice.restring.shadow; import android.content.res.AssetManager; import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowAssetManager; import java.util.LinkedHashMap; import java.util.Map; @Implements(AssetManager.class) public class MyShadowAssetManager extends ShadowAssetManager { private Map resourceEntryNames = new LinkedHashMap<>(); @Override public CharSequence getResourceText(int id) { return "@" + id; } @Override public String getResourceEntryName(int resid) { if (resourceEntryNames.containsKey(resid)) { return resourceEntryNames.get(resid); } return super.getResourceEntryName(resid); } public void addResourceEntryNameForTesting(int resId, String stringName) { resourceEntryNames.put(resId, stringName); } } ================================================ FILE: restring/src/test/java/com/ice/restring/shadow/MyShadowAsyncTask.java ================================================ package com.ice.restring.shadow; import android.os.AsyncTask; import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowAsyncTask; import java.util.concurrent.Executor; @Implements(AsyncTask.class) public class MyShadowAsyncTask extends ShadowAsyncTask { @Override public AsyncTask executeOnExecutor(Executor executor, Params... params) { return super.execute(params); } } ================================================ FILE: settings.gradle ================================================ include ':example', ':restring' ================================================ FILE: update_javadoc.sh ================================================ #!/bin/bash set -ex # Lib constants REPO="git@github.com:hamidness/restring.git" GROUP_ID="com.ice.restring" ARTIFACT_ID="restring" VERSION="1.0.0" # Javadoc constants FILE_PATH="${GROUP_ID//./%2F}%2F${ARTIFACT_ID}%2F${VERSION}%2F${ARTIFACT_ID}-${VERSION}-javadoc.jar" URL="https://bintray.com/hamidfri/maven/download_file?file_path=$FILE_PATH" # Script constants TEMP_DIR=temp-clone DOCS_DIR=docs BRANCH_NAME=update_javadoc # Delete temps rm -rf $TEMP_DIR # Clone the repo and create a new branch git clone $REPO $TEMP_DIR cd $TEMP_DIR git checkout -b $BRANCH_NAME # Delete docs rm -rf $DOCS_DIR # Download the latest javadoc mkdir $DOCS_DIR cd $DOCS_DIR curl -L "$URL" > javadoc.zip unzip javadoc.zip rm javadoc.zip cd .. # Commit and push new javadoc git add . git add -u git commit -m "Update javadoc at $(date)" git push --set-upstream origin $BRANCH_NAME # Cleanup cd .. rm -rf $TEMP_DIR