From 81996038462de1be86643e95d262933c4b96c551 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sat, 8 Jul 2017 20:25:23 -0700 Subject: Move components to separate modules --- about/.gitignore | 1 + about/build.gradle | 63 ++++++ about/progress-proguard.txt | 1 + .../ca/allanwang/kau/ExampleInstrumentedTest.java | 26 +++ about/src/main/AndroidManifest.xml | 1 + .../ca/allanwang/kau/about/AboutActivityBase.kt | 248 +++++++++++++++++++++ .../kotlin/ca/allanwang/kau/about/CutoutIItem.kt | 48 ++++ .../kotlin/ca/allanwang/kau/about/LibraryIItem.kt | 99 ++++++++ .../res/layout/kau_about_section_libraries.xml | 27 +++ .../src/main/res/layout/kau_about_section_main.xml | 46 ++++ about/src/main/res/layout/kau_activity_about.xml | 26 +++ about/src/main/res/layout/kau_iitem_cutout.xml | 9 + about/src/main/res/layout/kau_iitem_library.xml | 124 +++++++++++ .../res/transition/kau_about_return_downward.xml | 49 ++++ .../res/transition/kau_about_return_upwards.xml | 50 +++++ about/src/main/res/values/styles.xml | 9 + .../java/ca/allanwang/kau/ExampleUnitTest.java | 17 ++ 17 files changed, 844 insertions(+) create mode 100644 about/.gitignore create mode 100644 about/build.gradle create mode 100644 about/progress-proguard.txt create mode 100644 about/src/androidTest/java/ca/allanwang/kau/ExampleInstrumentedTest.java create mode 100644 about/src/main/AndroidManifest.xml create mode 100644 about/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt create mode 100644 about/src/main/kotlin/ca/allanwang/kau/about/CutoutIItem.kt create mode 100644 about/src/main/kotlin/ca/allanwang/kau/about/LibraryIItem.kt create mode 100644 about/src/main/res/layout/kau_about_section_libraries.xml create mode 100644 about/src/main/res/layout/kau_about_section_main.xml create mode 100644 about/src/main/res/layout/kau_activity_about.xml create mode 100644 about/src/main/res/layout/kau_iitem_cutout.xml create mode 100644 about/src/main/res/layout/kau_iitem_library.xml create mode 100644 about/src/main/res/transition/kau_about_return_downward.xml create mode 100644 about/src/main/res/transition/kau_about_return_upwards.xml create mode 100644 about/src/main/res/values/styles.xml create mode 100644 about/src/test/java/ca/allanwang/kau/ExampleUnitTest.java (limited to 'about') diff --git a/about/.gitignore b/about/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/about/.gitignore @@ -0,0 +1 @@ +/build diff --git a/about/build.gradle b/about/build.gradle new file mode 100644 index 0000000..5598429 --- /dev/null +++ b/about/build.gradle @@ -0,0 +1,63 @@ +plugins { + id 'com.gladed.androidgitversion' version '0.3.4' +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'com.github.dcendents.android-maven' + +group = project.APP_GROUP + +android { + compileSdkVersion Integer.parseInt(project.TARGET_SDK) + buildToolsVersion project.BUILD_TOOLS + + androidGitVersion { + codeFormat = 'MMNNPPBB' + prefix 'v' + } + + defaultConfig { + minSdkVersion Integer.parseInt(project.MIN_SDK) + targetSdkVersion Integer.parseInt(project.TARGET_SDK) + versionCode androidGitVersion.code() + versionName androidGitVersion.name() + consumerProguardFiles 'progress-proguard.txt' + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + checkReleaseBuilds false + } + resourcePrefix "kau_" + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + testCompile 'junit:junit:4.12' + + compile project(':core-ui') + compile project(':adapter') + + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + + compile("com.mikepenz:aboutlibraries:${ABOUT_LIBRARIES}@aar") { + transitive = true + } +} + +apply from: '../artifacts.gradle' diff --git a/about/progress-proguard.txt b/about/progress-proguard.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/about/progress-proguard.txt @@ -0,0 +1 @@ + diff --git a/about/src/androidTest/java/ca/allanwang/kau/ExampleInstrumentedTest.java b/about/src/androidTest/java/ca/allanwang/kau/ExampleInstrumentedTest.java new file mode 100644 index 0000000..7b079b2 --- /dev/null +++ b/about/src/androidTest/java/ca/allanwang/kau/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package ca.allanwang.kau; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("ca.allanwang.kau.test", appContext.getPackageName()); + } +} diff --git a/about/src/main/AndroidManifest.xml b/about/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9c9b9e6 --- /dev/null +++ b/about/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/about/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt b/about/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt new file mode 100644 index 0000000..4751a09 --- /dev/null +++ b/about/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt @@ -0,0 +1,248 @@ +package ca.allanwang.kau.about + +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.support.v4.view.PagerAdapter +import android.support.v4.view.ViewPager +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.RecyclerView +import android.transition.TransitionInflater +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import ca.allanwang.kau.R +import ca.allanwang.kau.adapters.FastItemThemedAdapter +import ca.allanwang.kau.adapters.ThemableIItemColors +import ca.allanwang.kau.adapters.ThemableIItemColorsDelegate +import ca.allanwang.kau.animators.FadeScaleAnimator +import ca.allanwang.kau.iitems.CutoutIItem +import ca.allanwang.kau.iitems.HeaderIItem +import ca.allanwang.kau.iitems.LibraryIItem +import ca.allanwang.kau.utils.* +import ca.allanwang.kau.ui.widgets.ElasticDragDismissFrameLayout +import ca.allanwang.kau.ui.widgets.InkPageIndicator +import com.mikepenz.aboutlibraries.Libs +import com.mikepenz.aboutlibraries.entity.Library +import com.mikepenz.fastadapter.IItem +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import java.security.InvalidParameterException + +/** + * Created by Allan Wang on 2017-06-28. + * + * Floating About Activity Panel for your app + * This contains all the necessary layouts, and can be extended and configured using the [configBuilder] + * The [rClass] is necessary to generate the list of libraries used in your app, and should point to your app's + * R.string::class.java + * If you don't need auto detect, you can pass null instead + * Note that for the auto detection to work, the R fields must be excluded from Proguard + * Manual lib listings and other extra modifications can be done so by overriding the open functions + */ +abstract class AboutActivityBase(val rClass: Class<*>?, val configBuilder: Configs.() -> Unit = {}) : AppCompatActivity(), ViewPager.OnPageChangeListener { + + val draggableFrame: ElasticDragDismissFrameLayout by bindView(R.id.about_draggable_frame) + val pager: ViewPager by bindView(R.id.about_pager) + val indicator: InkPageIndicator by bindView(R.id.about_indicator) + /** + * Holds some common configurations that may be added directly from the constructor + * Applied lazily since it needs the context to fetch resources + */ + val configs: Configs by lazy { Configs().apply { configBuilder() } } + /** + * Number of pages in the adapter + * Defaults to just the main view and lib view + */ + open val pageCount: Int = 2 + /** + * Page position for the libs + * This is generated automatically if [inflateLibPage] is called + */ + private var libPage: Int = -2 + /** + * Holds that status of each page + * 0 means nothing has happened + * 1 means this page has been in view at least once + * The rest is up to you + */ + lateinit var pageStatus: IntArray + /** + * Holds the lib items once they are fetched asynchronously + */ + var libItems: List? = null + /** + * Holds the adapter for the library page; this is generated later because it uses the config colors + */ + lateinit var libAdapter: FastItemThemedAdapter> + /** + * Global reference of the library recycler + * This is set by default through [inflateLibPage] and is used to stop scrolling + * When the draggable frame exits + * It is not required, hence its nullability + */ + private var libRecycler: RecyclerView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.kau_activity_about) + pageStatus = IntArray(pageCount) + libAdapter = FastItemThemedAdapter(configs) + LibraryIItem.bindClickEvents(libAdapter) + if (configs.textColor != null) indicator.setColour(configs.textColor!!) + with(pager) { + adapter = AboutPagerAdapter() + pageMargin = dimenPixelSize(R.dimen.kau_spacing_normal) + addOnPageChangeListener(this@AboutActivityBase) + } + indicator.setViewPager(pager) + draggableFrame.addListener(object : ElasticDragDismissFrameLayout.SystemChromeFader(this) { + override fun onDragDismissed() { + // if we drag dismiss downward then the default reversal of the enter + // transition would slide content upward which looks weird. So reverse it. + if (draggableFrame.translationY > 0) { + window.returnTransition = TransitionInflater.from(this@AboutActivityBase) + .inflateTransition(configs.transitionExitReversed) + } + libRecycler?.stopScroll() + finishAfterTransition() + } + }) + } + + inner class Configs : ThemableIItemColors by ThemableIItemColorsDelegate() { + var cutoutTextRes: Int = -1 + var cutoutText: String? = null + var cutoutDrawableRes: Int = -1 + var cutoutDrawable: Drawable? = null + var cutoutForeground: Int? = null + var libPageTitleRes: Int = -1 + var libPageTitle: String? = string(R.string.kau_about_libraries_intro) //This is in the string by default since it's lower priority + /** + * Transition to be called if the view is dragged down + */ + var transitionExitReversed: Int = R.transition.kau_about_return_downward + } + + /** + * Method to fetch the library list + * This is fetched asynchronously and you may override it to customize the list + */ + open fun getLibraries(libs: Libs): List = libs.prepareLibraries(this, null, null, true, true) + + /** + * Gets the view associated with the given page position + * Keep in mind that when inflating, do NOT add the view to the viewgroup + * Use layoutInflater.inflate(id, parent, false) + */ + open fun getPage(position: Int, layoutInflater: LayoutInflater, parent: ViewGroup): View { + return when (position) { + 0 -> inflateMainPage(layoutInflater, parent, position) + pageCount - 1 -> inflateLibPage(layoutInflater, parent, position) + else -> throw InvalidParameterException() + } + } + + /** + * Create the main view with the cutout + */ + open fun inflateMainPage(layoutInflater: LayoutInflater, parent: ViewGroup, position: Int): View { + val fastAdapter = FastItemThemedAdapter>(configs) + val recycler = fullLinearRecycler(fastAdapter) + fastAdapter.add(CutoutIItem { + with(configs) { + text = string(cutoutTextRes, cutoutText) + drawable = drawable(cutoutDrawableRes, cutoutDrawable) + if (configs.cutoutForeground != null) foregroundColor = configs.cutoutForeground!! + } + }.apply { + themeEnabled = configs.cutoutForeground == null + }) + postInflateMainPage(fastAdapter) + return recycler + } + + /** + * Open hook called just before the main page view is returned + * Feel free to add your own items to the adapter in here + */ + open fun postInflateMainPage(adapter: FastItemThemedAdapter>) { + + } + + /** + * Create the lib view with the list of libraries + */ + open fun inflateLibPage(layoutInflater: LayoutInflater, parent: ViewGroup, position: Int): View { + libPage = position + val v = layoutInflater.inflate(R.layout.kau_recycler_detached_background, parent, false) + val recycler = v.findViewById(R.id.kau_recycler_detached) + libRecycler = recycler + recycler.adapter = libAdapter + recycler.itemAnimator = FadeScaleAnimator(itemDelayFactor = 0.2f).apply { addDuration = 300; interpolator = AnimHolder.decelerateInterpolator(this@AboutActivityBase) } + val background = v.findViewById(R.id.kau_recycler_detached_background) + if (configs.backgroundColor != null) background.setBackgroundColor(configs.backgroundColor!!.colorToForeground()) + doAsync { + libItems = getLibraries( + if (rClass == null) Libs(this@AboutActivityBase) else Libs(this@AboutActivityBase, Libs.toStringArray(rClass.fields)) + ).map { LibraryIItem(it) } + if (libPage >= 0 && pageStatus[libPage] == 1) + uiThread { addLibItems() } + } + return v + } + + inner class AboutPagerAdapter : PagerAdapter() { + + private val layoutInflater: LayoutInflater = LayoutInflater.from(this@AboutActivityBase) + private val views = Array(pageCount) { null } + + override fun instantiateItem(collection: ViewGroup, position: Int): Any { + val layout = getPage(position, collection) + collection.addView(layout) + return layout + } + + override fun destroyItem(collection: ViewGroup, position: Int, view: Any) { + collection.removeView(view as View) + views[position] = null + } + + override fun getCount(): Int = pageCount + + override fun isViewFromObject(view: View, `object`: Any): Boolean = view === `object` + + /** + * Only get page if view does not exist + */ + private fun getPage(position: Int, parent: ViewGroup): View { + if (views[position] == null) views[position] = getPage(position, layoutInflater, parent) + return views[position]!! + } + } + + override fun onPageScrollStateChanged(state: Int) {} + + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} + + override fun onPageSelected(position: Int) { + if (pageStatus[position] == 0) pageStatus[position] = 1 // mark as seen if previously null + if (position == libPage && libItems != null && pageStatus[position] == 1) { + pageStatus[position] = 2 //add libs and mark as such + postDelayed(300) { addLibItems() } //delay so that the animations occur once the page is fully switched + } + } + + /** + * Function that is called when the view is ready to add the lib items + * Feel free to add your own items here + */ + open fun addLibItems() { + libAdapter.add(HeaderIItem(text = configs.libPageTitle, textRes = configs.libPageTitleRes)) + .add(libItems) + } + + override fun onDestroy() { + AnimHolder.decelerateInterpolator.invalidate() //clear the reference to the interpolators we've used + super.onDestroy() + } +} \ No newline at end of file diff --git a/about/src/main/kotlin/ca/allanwang/kau/about/CutoutIItem.kt b/about/src/main/kotlin/ca/allanwang/kau/about/CutoutIItem.kt new file mode 100644 index 0000000..34e8641 --- /dev/null +++ b/about/src/main/kotlin/ca/allanwang/kau/about/CutoutIItem.kt @@ -0,0 +1,48 @@ +package ca.allanwang.kau.about + +import android.support.v7.widget.RecyclerView +import android.view.View +import ca.allanwang.kau.R +import ca.allanwang.kau.adapters.ThemableIItem +import ca.allanwang.kau.adapters.ThemableIItemDelegate +import ca.allanwang.kau.ui.views.CutoutView +import ca.allanwang.kau.utils.bindView +import com.mikepenz.fastadapter.items.AbstractItem + +/** + * Created by Allan Wang on 2017-06-28. + * + * Just a cutout item with some defaults in [R.layout.kau_iitem_cutout] + */ +class CutoutIItem(val config: CutoutView.() -> Unit = {} +) : AbstractItem(), ThemableIItem by ThemableIItemDelegate() { + + override fun getType(): Int = R.id.kau_item_cutout + + override fun getLayoutRes(): Int = R.layout.kau_iitem_cutout + + override fun isSelectable(): Boolean = false + + override fun bindView(holder: ViewHolder, payloads: MutableList?) { + super.bindView(holder, payloads) + with(holder) { + if (accentColor != null && themeEnabled) cutout.foregroundColor = accentColor!! + cutout.config() + } + } + + override fun unbindView(holder: ViewHolder) { + super.unbindView(holder) + with(holder) { + cutout.drawable = null + cutout.text = "Text" //back to default + } + } + + override fun getViewHolder(v: View): ViewHolder = ViewHolder(v) + + class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { + val cutout: CutoutView by bindView(R.id.kau_cutout) + } + +} \ No newline at end of file diff --git a/about/src/main/kotlin/ca/allanwang/kau/about/LibraryIItem.kt b/about/src/main/kotlin/ca/allanwang/kau/about/LibraryIItem.kt new file mode 100644 index 0000000..1b832a2 --- /dev/null +++ b/about/src/main/kotlin/ca/allanwang/kau/about/LibraryIItem.kt @@ -0,0 +1,99 @@ +package ca.allanwang.kau.about + +import android.os.Build +import android.support.v7.widget.CardView +import android.support.v7.widget.RecyclerView +import android.text.Html +import android.view.View +import android.widget.TextView +import ca.allanwang.kau.R +import ca.allanwang.kau.adapters.ThemableIItem +import ca.allanwang.kau.adapters.ThemableIItemDelegate +import ca.allanwang.kau.utils.bindView +import ca.allanwang.kau.utils.gone +import ca.allanwang.kau.utils.startLink +import ca.allanwang.kau.utils.visible +import com.mikepenz.aboutlibraries.entity.Library +import com.mikepenz.fastadapter.FastAdapter +import com.mikepenz.fastadapter.IItem +import com.mikepenz.fastadapter.items.AbstractItem + +/** + * Created by Allan Wang on 2017-06-27. + */ +class LibraryIItem(val lib: Library +) : AbstractItem(), ThemableIItem by ThemableIItemDelegate() { + + companion object { + @JvmStatic fun bindClickEvents(fastAdapter: FastAdapter>) { + fastAdapter.withOnClickListener { v, _, item, _ -> + if (item !is LibraryIItem) false + else { + val c = v.context + with(item.lib) { + c.startLink(libraryWebsite, repositoryLink, authorWebsite) + } + true + } + } + } + } + + override fun getType(): Int = R.id.kau_item_library + + override fun getLayoutRes(): Int = R.layout.kau_iitem_library + + override fun isSelectable(): Boolean = false + + override fun bindView(holder: ViewHolder, payloads: MutableList?) { + super.bindView(holder, payloads) + with(holder) { + name.text = lib.libraryName + creator.text = lib.author + description.text = if (lib.libraryDescription.isBlank()) lib.libraryDescription + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(lib.libraryDescription, Html.FROM_HTML_MODE_LEGACY) + else Html.fromHtml(lib.libraryDescription) + bottomDivider.gone() + if (lib.libraryVersion?.isNotBlank() ?: false) { + bottomDivider.visible() + version.visible().text = lib.libraryVersion + } + if (lib.license?.licenseName?.isNotBlank() ?: false) { + bottomDivider.visible() + license.visible().text = lib.license?.licenseName + } + bindTextColor(name, creator) + bindTextColorSecondary(description) + bindAccentColor(license, version) + bindDividerColor(divider, bottomDivider) + bindBackgroundRipple(card) + } + } + + override fun unbindView(holder: ViewHolder) { + super.unbindView(holder) + with(holder) { + name.text = null + creator.text = null + description.text = null + bottomDivider.gone() + version.gone().text = null + license.gone().text = null + } + } + + override fun getViewHolder(v: View): ViewHolder = ViewHolder(v) + + class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { + val card: CardView by bindView(R.id.lib_item_card) + val name: TextView by bindView(R.id.lib_item_name) + val creator: TextView by bindView(R.id.lib_item_author) + val description: TextView by bindView(R.id.lib_item_description) + val version: TextView by bindView(R.id.lib_item_version) + val license: TextView by bindView(R.id.lib_item_license) + val divider: View by bindView(R.id.lib_item_top_divider) + val bottomDivider: View by bindView(R.id.lib_item_bottom_divider) + } + +} \ No newline at end of file diff --git a/about/src/main/res/layout/kau_about_section_libraries.xml b/about/src/main/res/layout/kau_about_section_libraries.xml new file mode 100644 index 0000000..c14225e --- /dev/null +++ b/about/src/main/res/layout/kau_about_section_libraries.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/about/src/main/res/layout/kau_about_section_main.xml b/about/src/main/res/layout/kau_about_section_main.xml new file mode 100644 index 0000000..40d8dfb --- /dev/null +++ b/about/src/main/res/layout/kau_about_section_main.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + diff --git a/about/src/main/res/layout/kau_activity_about.xml b/about/src/main/res/layout/kau_activity_about.xml new file mode 100644 index 0000000..3d1f9ab --- /dev/null +++ b/about/src/main/res/layout/kau_activity_about.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/about/src/main/res/layout/kau_iitem_cutout.xml b/about/src/main/res/layout/kau_iitem_cutout.xml new file mode 100644 index 0000000..b3a841e --- /dev/null +++ b/about/src/main/res/layout/kau_iitem_cutout.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/about/src/main/res/layout/kau_iitem_library.xml b/about/src/main/res/layout/kau_iitem_library.xml new file mode 100644 index 0000000..1c3de5c --- /dev/null +++ b/about/src/main/res/layout/kau_iitem_library.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/about/src/main/res/transition/kau_about_return_downward.xml b/about/src/main/res/transition/kau_about_return_downward.xml new file mode 100644 index 0000000..b040b1b --- /dev/null +++ b/about/src/main/res/transition/kau_about_return_downward.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/about/src/main/res/transition/kau_about_return_upwards.xml b/about/src/main/res/transition/kau_about_return_upwards.xml new file mode 100644 index 0000000..64b3f5e --- /dev/null +++ b/about/src/main/res/transition/kau_about_return_upwards.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/about/src/main/res/values/styles.xml b/about/src/main/res/values/styles.xml new file mode 100644 index 0000000..59b2470 --- /dev/null +++ b/about/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/about/src/test/java/ca/allanwang/kau/ExampleUnitTest.java b/about/src/test/java/ca/allanwang/kau/ExampleUnitTest.java new file mode 100644 index 0000000..a29b447 --- /dev/null +++ b/about/src/test/java/ca/allanwang/kau/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package ca.allanwang.kau; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file -- cgit v1.2.3