diff options
Diffstat (limited to 'core')
15 files changed, 191 insertions, 59 deletions
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kotlin/Streams.kt b/core/src/main/kotlin/ca/allanwang/kau/kotlin/Streams.kt new file mode 100644 index 0000000..5dc44a8 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/kotlin/Streams.kt @@ -0,0 +1,17 @@ +package ca.allanwang.kau.kotlin + +/** + * Created by Allan Wang on 2017-08-05. + */ + +/** + * Replica of [Vector.removeIf] in Java + * Since we don't have access to the internals of our extended class, + * We will simply iterate and remove when the filter returns {@code false} + */ +@Synchronized inline fun <T, C : MutableIterable<T>> C.kauRemoveIf(filter: (item: T) -> Boolean): C { + val iter = iterator() + while (iter.hasNext()) + if (filter(iter.next())) iter.remove() + return this +}
\ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt b/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt index e0f6cae..4c6d655 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt @@ -1,3 +1,5 @@ +@file:Suppress("NOTHING_TO_INLINE") + package ca.allanwang.kau.logging import timber.log.Timber @@ -9,11 +11,11 @@ import timber.log.Timber * Timber extension that will embed the tag as part of the message for each log item */ open class TimberLogger(tag: String) { - internal val TAG = "$tag: %s" - fun e(s: String) = Timber.e(TAG, s) - fun e(t: Throwable?, s: String = "error") = if (t == null) e(s) else Timber.e(t, TAG, s) - fun d(s: String) = Timber.d(TAG, s) - fun i(s: String) = Timber.i(TAG, s) - fun v(s: String) = Timber.v(TAG, s) - fun eThrow(s: String) = e(Throwable(s)) + val TAG = "$tag: %s" + inline fun e(s: String) = Timber.e(TAG, s) + inline fun e(t: Throwable?, s: String = "error") = if (t == null) e(s) else Timber.e(t, TAG, s) + inline fun d(s: String) = Timber.d(TAG, s) + inline fun i(s: String) = Timber.i(TAG, s) + inline fun v(s: String) = Timber.v(TAG, s) + inline fun eThrow(s: String) = e(Throwable(s)) }
\ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/RelativeSlider.kt b/core/src/main/kotlin/ca/allanwang/kau/swipe/RelativeSlider.kt index 2ffb2ef..f14f5cf 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/swipe/RelativeSlider.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/RelativeSlider.kt @@ -1,7 +1,5 @@ package ca.allanwang.kau.swipe -import ca.allanwang.kau.kotlin.nonReadable - /** * Created by Mr.Jude on 2015/8/26. * @@ -9,13 +7,12 @@ import ca.allanwang.kau.kotlin.nonReadable * * Helper class to give the previous activity an offset as the main page is pulled */ -class RelativeSlider(var curPage: SwipeBackPage) : SwipeListener { +internal class RelativeSlider(var curPage: SwipeBackPage) : SwipeListener { var offset = 0f var enabled: Boolean - @Deprecated(level = DeprecationLevel.ERROR, message = "Cannot use enabled as getter") - get() = nonReadable() + get() = curPage.hasListener(this) set(value) { if (value) curPage.addListener(this) else curPage.removeListener(this) diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackHelper.kt b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackHelper.kt index 503f1fc..0859ac5 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackHelper.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackHelper.kt @@ -2,27 +2,27 @@ package ca.allanwang.kau.swipe import android.app.Activity import ca.allanwang.kau.R +import ca.allanwang.kau.kotlin.kauRemoveIf +import ca.allanwang.kau.logging.KL import ca.allanwang.kau.swipe.SwipeBackHelper.onDestroy import java.util.* -class SwipeBackException(message: String = "You Should call kauSwipeOnCreate() first") : RuntimeException(message) +internal class SwipeBackException(message: String = "You Should call kauSwipeOnCreate() first") : RuntimeException(message) /** * Singleton to hold our swipe stack * All activity pages held with strong references, so it is crucial to call * [onDestroy] whenever an activity should be disposed */ -object SwipeBackHelper { +internal object SwipeBackHelper { private val pageStack = Stack<SwipeBackPage>() - private operator fun get(activity: Activity): SwipeBackPage - = pageStack.firstOrNull { it.activity === activity } ?: throw SwipeBackException() - - fun getCurrentPage(activity: Activity): SwipeBackPage = this[activity] + private operator fun get(activity: Activity): SwipeBackPage? + = pageStack.firstOrNull { it.activityRef.get() === activity } fun onCreate(activity: Activity, builder: SwipeBackContract.() -> Unit = {}) { - val page = pageStack.firstOrNull { it.activity === activity } ?: pageStack.push(SwipeBackPage(activity).apply { builder() }) + val page = this[activity] ?: pageStack.push(SwipeBackPage(activity).apply { builder() }) val startAnimation: Int = when (page.edgeFlag) { SWIPE_EDGE_LEFT -> R.anim.kau_slide_in_right SWIPE_EDGE_RIGHT -> R.anim.kau_slide_in_left @@ -30,23 +30,28 @@ object SwipeBackHelper { else -> R.anim.kau_slide_in_top } activity.overridePendingTransition(startAnimation, 0) + KL.v("KauSwipe onCreate ${activity.localClassName}") } - fun onPostCreate(activity: Activity) = this[activity].onPostCreate() + fun onPostCreate(activity: Activity) { + this[activity]?.onPostCreate() ?: throw SwipeBackException() + KL.v("KauSwipe onPostCreate ${activity.localClassName}") + } fun onDestroy(activity: Activity) { - val page: SwipeBackPage = this[activity] - pageStack.remove(page) - page.activity = null + val page: SwipeBackPage? = this[activity] + pageStack.kauRemoveIf { it.activityRef.get() == null || it === page } + page?.activityRef?.clear() + KL.v("KauSwipe onDestroy ${activity.localClassName}") } - fun finish(activity: Activity) = this[activity].scrollToFinishActivity() + fun finish(activity: Activity) = this[activity]?.scrollToFinishActivity() - internal fun getPrePage(activity: SwipeBackPage): SwipeBackPage? { - val index = pageStack.indexOf(activity) - return if (index > 0) pageStack[index - 1] else null + internal fun getPrePage(page: SwipeBackPage): SwipeBackPage? { + //clean invalid pages + pageStack.kauRemoveIf { it.activityRef.get() == null } + return pageStack.getOrNull(pageStack.indexOf(page) - 1) } - } /** diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackLayout.kt b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackLayout.kt index 1474c1a..51cd17f 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackLayout.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackLayout.kt @@ -10,6 +10,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import ca.allanwang.kau.logging.KL import ca.allanwang.kau.utils.adjustAlpha import ca.allanwang.kau.utils.navigationBarColor import ca.allanwang.kau.utils.statusBarColor @@ -22,8 +23,8 @@ import java.lang.ref.WeakReference * If an edge detection occurs, this layout consumes all the touch events * Use the [swipeEnabled] toggle if you need the scroll events on the same axis */ -class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 -) : FrameLayout(context, attrs, defStyle), SwipeBackContract { +internal class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 +) : FrameLayout(context, attrs, defStyle), SwipeBackContract, SwipeBackContractInternal { override val swipeBackLayout: SwipeBackLayout get() = this @@ -51,7 +52,7 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu override var disallowIntercept = false - private var contentView: View? = null + private lateinit var contentViewRef: WeakReference<View> private val dragHelper: ViewDragHelper @@ -156,16 +157,6 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu addListener(chromeFadeListener) } - - /** - * Set up contentView which will be moved by user gesture - - * @param view - */ - private fun setContentView(view: View) { - contentView = view - } - override fun setEdgeSizePercent(swipeEdgePercent: Float) { edgeSize = ((if (horizontal) resources.displayMetrics.widthPixels else resources.displayMetrics.heightPixels) * swipeEdgePercent).toInt() } @@ -181,6 +172,7 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu /** * Removes a listener from the set of listeners + * and scans our list for invalid ones * @param listener */ @@ -194,10 +186,27 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu } /** + * Checks if a listener exists in our list, + * and remove invalid ones at the same time + */ + override fun hasListener(listener: SwipeListener): Boolean { + val iter = listeners.iterator() + while (iter.hasNext()) { + val l = iter.next().get() + if (l == null) + iter.remove() + else if (l == listener) + return true + } + return false + } + + /** * Scroll out contentView and finish the activity */ override fun scrollToFinishActivity() { - val childWidth = contentView!!.width + val contentView = contentViewRef.get() ?: return KL.e("KauSwipe cannot scroll to finish as contentView is null. Is onPostCreate called?") + val childWidth = contentView.width val top = 0 val left = childWidth + OVERSCROLL_DISTANCE dragHelper.smoothSlideViewTo(contentView, left, top) @@ -224,6 +233,7 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + val contentView = contentViewRef.get() ?: return KL.e("KauSwipe cannot change layout as contentView is null. Is onPostCreate called?") inLayout = true val xOffset: Int val yOffset: Int @@ -234,7 +244,7 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu xOffset = 0 yOffset = contentOffset } - contentView?.layout(xOffset, yOffset, xOffset + contentView!!.measuredWidth, yOffset + contentView!!.measuredHeight) + contentView.layout(xOffset, yOffset, xOffset + contentView.measuredWidth, yOffset + contentView.measuredHeight) inLayout = false } @@ -243,7 +253,7 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu } override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean { - val drawContent = child === contentView + val drawContent = child === contentViewRef.get() val ret = super.drawChild(canvas, child, drawingTime) if (scrimOpacity > 0 && drawContent && dragHelper.viewDragState != ViewDragHelper.STATE_IDLE) drawScrim(canvas, child) @@ -272,7 +282,7 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu val contentChild = content.getChildAt(0) content.removeView(contentChild) addView(contentChild) - setContentView(contentChild) + contentViewRef = WeakReference(contentChild) content.addView(this) } @@ -286,6 +296,7 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu content.removeView(this) removeView(contentChild) content.addView(contentChild) + contentViewRef.clear() } override fun computeScroll() { @@ -324,10 +335,11 @@ class SwipeBackLayout @JvmOverloads constructor(context: Context, attrs: Attribu override fun onViewPositionChanged(changedView: View, left: Int, top: Int, dx: Int, dy: Int) { super.onViewPositionChanged(changedView, left, top, dx, dy) + val contentView = contentViewRef.get() ?: return KL.e("KauSwipe cannot change view position as contentView is null; is onPostCreate called?") //make sure that we are using the proper axis scrollPercent = Math.abs( - if (horizontal) left.toFloat() / contentView!!.width - else (top.toFloat() / contentView!!.height)) + if (horizontal) left.toFloat() / contentView.width + else (top.toFloat() / contentView.height)) contentOffset = if (horizontal) left else top invalidate() if (scrollPercent < scrollThreshold && !isScrollOverValid) diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackPage.kt b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackPage.kt index 81ddb62..3132f8c 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackPage.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/SwipeBackPage.kt @@ -4,15 +4,17 @@ import android.app.Activity import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.view.ViewGroup +import ca.allanwang.kau.logging.KL +import java.lang.ref.WeakReference /** * Created by Mr.Jude on 2015/8/3. * * Updated by Allan Wang on 2017/07/05 */ -class SwipeBackPage(activity: Activity) : SwipeBackContract by SwipeBackLayout(activity) { +internal class SwipeBackPage(activity: Activity) : SwipeBackContractInternal by SwipeBackLayout(activity) { - var activity: Activity? = activity + var activityRef = WeakReference(activity) var slider: RelativeSlider /** @@ -38,8 +40,9 @@ class SwipeBackPage(activity: Activity) : SwipeBackContract by SwipeBackLayout(a } private fun handleLayout() { - if (swipeEnabled) swipeBackLayout.attachToActivity(activity!!) - else swipeBackLayout.removeFromActivity(activity!!) + val activity = activityRef.get() ?: return KL.v("KauSwipe activity ref gone during handleLayout") + if (swipeEnabled) swipeBackLayout.attachToActivity(activity) + else swipeBackLayout.removeFromActivity(activity) } fun setClosePercent(percent: Float): SwipeBackPage { @@ -49,6 +52,10 @@ class SwipeBackPage(activity: Activity) : SwipeBackContract by SwipeBackLayout(a } +internal interface SwipeBackContractInternal : SwipeBackContract { + val swipeBackLayout: SwipeBackLayout +} + interface SwipeBackContract { /** * Toggle main touch intercept @@ -59,7 +66,6 @@ interface SwipeBackContract { * This dynamically fades as the page gets closer to exiting */ var scrimColor: Int - val swipeBackLayout: SwipeBackLayout var edgeSize: Int /** * Set the flag for which edge the page is scrolling from @@ -92,7 +98,9 @@ interface SwipeBackContract { * Sets edge size based on screen size */ fun setEdgeSizePercent(swipeEdgePercent: Float) + fun addListener(listener: SwipeListener) fun removeListener(listener: SwipeListener) + fun hasListener(listener: SwipeListener): Boolean fun scrollToFinishActivity() }
\ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/swipe/ViewDragHelper.java b/core/src/main/kotlin/ca/allanwang/kau/swipe/ViewDragHelper.java index 3368e10..566e9e5 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/swipe/ViewDragHelper.java +++ b/core/src/main/kotlin/ca/allanwang/kau/swipe/ViewDragHelper.java @@ -28,7 +28,7 @@ import static ca.allanwang.kau.swipe.SwipeBackHelperKt.SWIPE_EDGE_TOP; * This is an extension of {@link android.support.v4.widget.ViewDragHelper} * Along with additional methods defined in {@link ViewDragHelperExtras} */ -public class ViewDragHelper implements ViewDragHelperExtras { +class ViewDragHelper implements ViewDragHelperExtras { private static final String TAG = "ViewDragHelper"; /** diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt index dad01f1..889f347 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt @@ -1,14 +1,16 @@ package ca.allanwang.kau.utils +import android.support.v4.widget.ViewDragHelper + /** * Created by Allan Wang on 2017-06-08. */ const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android" -const val KAU_LEFT = 1 -const val KAU_TOP = 2 -const val KAU_RIGHT = 4 -const val KAU_BOTTOM = 8 +const val KAU_LEFT = ViewDragHelper.EDGE_LEFT +const val KAU_RIGHT = ViewDragHelper.EDGE_RIGHT +const val KAU_TOP = ViewDragHelper.EDGE_TOP +const val KAU_BOTTOM = ViewDragHelper.EDGE_BOTTOM const val KAU_HORIZONTAL = KAU_LEFT or KAU_RIGHT const val KAU_VERTICAL = KAU_TOP or KAU_BOTTOM const val KAU_ALL = KAU_HORIZONTAL or KAU_VERTICAL diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/FileUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/FileUtils.kt new file mode 100644 index 0000000..25e0519 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/FileUtils.kt @@ -0,0 +1,33 @@ +package ca.allanwang.kau.utils + +import android.content.Context +import android.os.Environment +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.text.SimpleDateFormat +import java.util.* + +/** + * Created by Allan Wang on 2017-08-04. + */ +@Throws(IOException::class) +fun createMediaFile(prefix: String, extension: String): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val imageFileName = "${prefix}_${timeStamp}_" + val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + val frostDir = File(storageDir, prefix) + if (!frostDir.exists()) frostDir.mkdirs() + return File.createTempFile(imageFileName, extension, frostDir) +} + +@Throws(IOException::class) +fun Context.createPrivateMediaFile(prefix: String, extension: String): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val imageFileName = "${prefix}_${timeStamp}_" + val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES) + return File.createTempFile(imageFileName, extension, storageDir) +} + +fun File.copyFromInputStream(inputStream: InputStream) + = inputStream.use { input -> outputStream().use { output -> input.copyTo(output) } }
\ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/NotificationUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/NotificationUtils.kt new file mode 100644 index 0000000..23a8370 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/NotificationUtils.kt @@ -0,0 +1,11 @@ +package ca.allanwang.kau.utils + +import android.content.Context +import android.support.v4.app.NotificationManagerCompat + + +/** + * Created by Allan Wang on 2017-08-04. + */ +fun Context.cancelNotification(notifId: Int) + = NotificationManagerCompat.from(this).cancel(notifId)
\ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt index 42d150e..538208d 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt @@ -1,6 +1,5 @@ package ca.allanwang.kau.utils -import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageManager import android.os.Build diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt index fa062f7..1f4536b 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt @@ -6,9 +6,9 @@ import android.support.annotation.TransitionRes import android.support.transition.AutoTransition import android.support.transition.TransitionInflater import android.support.transition.TransitionManager -import android.support.transition.Transition as SupportTransition import android.transition.Transition import android.view.ViewGroup +import android.support.transition.Transition as SupportTransition /** * Created by Allan Wang on 2017-06-24. diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/LazyResettableTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/LazyResettableTest.kt index 1997bd1..2025422 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/kotlin/LazyResettableTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/LazyResettableTest.kt @@ -7,6 +7,8 @@ import kotlin.test.assertNotEquals /** * Created by Allan Wang on 2017-07-29. + * + * Test code for [LazyResettable] */ class LazyResettableTest { diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/StreamsTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/StreamsTest.kt new file mode 100644 index 0000000..1c40f57 --- /dev/null +++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/StreamsTest.kt @@ -0,0 +1,42 @@ +package ca.allanwang.kau.kotlin + +import org.junit.Test +import kotlin.test.assertEquals + +/** + * Created by Allan Wang on 2017-08-05. + * + * Test code for [kauRemoveIf] + */ +class StreamsTest { + + @Test + fun basic() { + val items = mutableListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) + items.kauRemoveIf { it % 2 == 0 } + assertEquals(listOf(1, 3, 5, 7, 9), items) + } + + @Test + fun objectReference() { + data class Potato(val id: Int) + + val thePotato = Potato(9) + val items = mutableListOf<Potato>() + val result = mutableListOf<Potato>() + for (i in 0..11) { + val potato = Potato(i) + items.add(potato) + result.add(potato) + } + items.add(3, thePotato) + assertEquals(result.size + 1, items.size, "Invalid list addition") + assertEquals(2, items.filter { it.id == 9 }.size, "Invalid number of potatoes with id 9") + items.kauRemoveIf { it === thePotato } //removal by reference + assertEquals(result.size, items.size, "Invalid list size after removal") + assertEquals(result, items) + items.kauRemoveIf { it == thePotato } //removal by equality + assertEquals(result.size - 1, items.size, "Invalid list removal based on equality") + } + +}
\ No newline at end of file diff --git a/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt b/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt index 071ee9c..ce2b757 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt @@ -6,6 +6,8 @@ import kotlin.test.assertEquals /** * Created by Allan Wang on 2017-06-23. + * + * Misc test code */ class UtilsTest { |