Skip to content

Commit

Permalink
Merge branch 'main' into albendz-cc
Browse files Browse the repository at this point in the history
  • Loading branch information
albendz authored Oct 31, 2024
2 parents e73eac1 + 8137617 commit e760e14
Show file tree
Hide file tree
Showing 26 changed files with 241 additions and 155 deletions.
59 changes: 59 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ Scribe-Android uses pre-commit hooks to maintain a clean and consistent codebase

## Testing [`⇧`](#contents)

Writing unit tests is essential to guarantee the dependability and sustainability of the Scribe-Android codebase. Unit tests confirm that individual components of the application work as intended, detecting errors at an early stage, making the debugging process easier and boosting assurance for upcoming modifications. An unchanging testing method helps new team members grasp project norms and anticipated actions.

In addition to the [pre-commit](https://pre-commit.com/) hooks that are set up during the [development environment section](#dev-env), Scribe-Android includes a testing suite that should be ran before all pull requests and subsequent commits. Please run the following in the project root:

```bash
Expand All @@ -159,6 +161,63 @@ ktlint --format

<a id="issues-projects"></a>

### Unit Testing Conventions for Scribe-Android

To maintain consistency and clarity in the Scribe-Android codebase, we recommend you to follow these conventions when writing unit tests. These guidelines cover the organization, naming conventions, scope, and structure for unit tests.

#### 1. Project Structure for Unit Tests

- **Location**: Place all unit tests in the `src/test/java` directory to mirror the structure of the `src/main/java` directory. For new classes or features, ensure their corresponding test classes follow the same package structure.
- **Class Files**: Each class in `src/main/java` should have a dedicated test file in `src/test/java`, named by appending `Test` to the class name (e.g., `UserManager``UserManagerTest`).
- **New Classes for Testing**: When a new utility or helper class is needed specifically for testing, place it under `src/test/java/utils` or `src/test/java/helpers`.

#### 2. Naming Conventions for Tests

- **Test Methods**: Use descriptive names indicating expected behavior. Follow the format:

```kotlin
@Test
public void methodName_StateUnderTest_ExpectedBehavior() {
// Test code here
}
```

Example: `saveUser_WithValidData_SavesUserCorrectly()`.
#### 3. Scope and Focus of Tests
- **Single Responsibility**: Each test should cover only one behavior or scenario. For multiple behaviors, split them into separate test methods.
- **Setup and Teardown**: Use `@Before` for initializing objects and `@After` for cleanup, ensuring tests run in isolation.
```kotlin
@Before
public void setUp() {
// Initialize objects
}
@After
public void tearDown() {
// Cleanup objects
}
```
- **Mocking**: Use mocks (e.g., MockK) to isolate the unit test, especially with dependencies like databases, network requests or services.
#### 4. Writing Effective Tests
- **AAA Pattern (Arrange, Act, Assert)**: Structure each test with three distinct parts:
- **Arrange**: Set up the conditions.
- **Act**: Execute the method under test.
- **Assert**: Verify the result.
- **Coverage of Edge Cases**: Write tests for both typical cases and edge cases, like `null` values or invalid data.
#### 5. Test Documentation
- **Comments**: Add comments when test logic is complex or non-intuitive.
- **Assertions**: Use descriptive assertion methods (`assertTrue`, `assertEquals`, etc.) for clarity and include failure messages for custom assertions if necessary.
# Issues and projects [`⇧`](#contents)
The [issue tracker for Scribe-Android](https://github.com/scribe-org/Scribe-Android/issues) is the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests) and [submitting pull requests](#pull-requests). Scribe also organizes related issues into [projects](https://github.com/scribe-org/Scribe-Android/projects).
Expand Down
18 changes: 13 additions & 5 deletions app/src/main/java/be/scri/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class MainActivity : AppCompatActivity() {
supportActionBar?.setCustomView(R.layout.custom_action_bar_layout)
supportActionBar?.elevation = 0F
val layoutParams = supportActionBar?.customView?.layoutParams
layoutParams?.height = 1000
layoutParams?.height = DEFAULT_HEIGHT
supportActionBar?.customView?.layoutParams = layoutParams
setActionBarTitle(R.string.app_launcher_name)
val mButton = supportActionBar?.customView?.findViewById<Button>(R.id.button)
Expand Down Expand Up @@ -160,11 +160,11 @@ class MainActivity : AppCompatActivity() {
val textView = supportActionBar?.customView?.findViewById<TextView>(R.id.name)
val params = textView?.layoutParams as ViewGroup.MarginLayoutParams
if (shouldShowOnScreen) {
params.topMargin = -50
params.bottomMargin = 30
params.topMargin = ACTION_BAR_TOP_MARGIN_VISIBLE
params.bottomMargin = ACTION_BAR_BOTTOM_MARGIN_VISIBLE
} else {
params.topMargin = 50
params.bottomMargin = 0
params.topMargin = ACTION_BAR_TOP_MARGIN_HIDDEN
params.bottomMargin = ACTION_BAR_BOTTOM_MARGIN_HIDDEN
}
textView.layoutParams = params
}
Expand Down Expand Up @@ -215,4 +215,12 @@ class MainActivity : AppCompatActivity() {
val hintLayout = findViewById<View>(R.id.hint_layout)
hintLayout.visibility = View.GONE
}

companion object {
private const val DEFAULT_HEIGHT = 1000
private const val ACTION_BAR_TOP_MARGIN_VISIBLE = -50
private const val ACTION_BAR_TOP_MARGIN_HIDDEN = 50
private const val ACTION_BAR_BOTTOM_MARGIN_VISIBLE = 30
private const val ACTION_BAR_BOTTOM_MARGIN_HIDDEN = 0
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/be/scri/extensions/ContextStyling.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import android.graphics.Color
import be.scri.R
import be.scri.helpers.DARK_GREY

// handle system default theme (Material You) specially as the color is taken from the system, not hardcoded by us
// Handle system default theme (Material You) specially as the color is taken from the system, not hardcoded by us.
fun Context.getProperTextColor() =
if (baseConfig.isUsingSystemTheme) {
resources.getColor(R.color.you_neutral_text_color, theme)
Expand Down
35 changes: 26 additions & 9 deletions app/src/main/java/be/scri/extensions/Int.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@ import android.graphics.Color
import be.scri.helpers.DARK_GREY
import java.util.Random

private const val RED_COEFFICIENT = 299
private const val GREEN_COEFFICIENT = 587
private const val BLUE_COEFFICIENT = 114
private const val COEFFICIENT_SUM = 1000
private const val Y_THRESHOLD = 149

fun Int.getContrastColor(): Int {
val y = (299 * Color.red(this) + 587 * Color.green(this) + 114 * Color.blue(this)) / 1000
return if (y >= 149 && this != Color.BLACK) DARK_GREY else Color.WHITE
val y =
(
RED_COEFFICIENT * Color.red(this) +
GREEN_COEFFICIENT * Color.green(this) +
BLUE_COEFFICIENT * Color.blue(this)
) / COEFFICIENT_SUM
return if (y >= Y_THRESHOLD && this != Color.BLACK) DARK_GREY else Color.WHITE
}

fun Int.adjustAlpha(factor: Float): Int {
Expand All @@ -19,17 +30,21 @@ fun Int.adjustAlpha(factor: Float): Int {

fun ClosedRange<Int>.random() = Random().nextInt(endInclusive - start) + start

// taken from https://stackoverflow.com/a/40964456/1967672
fun Int.darkenColor(factor: Int = 8): Int {
// Taken from https://stackoverflow.com/a/40964456/1967672.
private const val HSV_COMPONENT_COUNT = 3
private const val DEFAULT_DARKEN_FACTOR = 8
private const val FACTOR_DIVIDER = 100

fun Int.darkenColor(factor: Int = DEFAULT_DARKEN_FACTOR): Int {
if (this == Color.WHITE || this == Color.BLACK) {
return this
}

val darkFactor = factor
var hsv = FloatArray(3)
var hsv = FloatArray(HSV_COMPONENT_COUNT)
Color.colorToHSV(this, hsv)
val hsl = hsv2hsl(hsv)
hsl[2] -= darkFactor / 100f
hsl[2] -= darkFactor / FACTOR_DIVIDER.toFloat()
if (hsl[2] < 0) {
hsl[2] = 0f
}
Expand All @@ -43,22 +58,24 @@ fun Int.lightenColor(factor: Int = 8): Int {
}

val lightFactor = factor
var hsv = FloatArray(3)
var hsv = FloatArray(HSV_COMPONENT_COUNT)
Color.colorToHSV(this, hsv)
val hsl = hsv2hsl(hsv)
hsl[2] += lightFactor / 100f
hsl[2] += lightFactor / FACTOR_DIVIDER.toFloat()
if (hsl[2] < 0) {
hsl[2] = 0f
}
hsv = hsl2hsv(hsl)
return Color.HSVToColor(hsv)
}

private const val LIGHTNESS_THRESHOLD = 0.5f

private fun hsl2hsv(hsl: FloatArray): FloatArray {
val hue = hsl[0]
var sat = hsl[1]
val light = hsl[2]
sat *= if (light < .5) light else 1 - light
sat *= if (light < LIGHTNESS_THRESHOLD) light else 1 - light
return floatArrayOf(hue, 2f * sat / (light + sat), light + sat)
}

Expand Down
51 changes: 20 additions & 31 deletions app/src/main/java/be/scri/fragments/LanguageSettingsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@ class LanguageSettingsFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewpager = requireActivity().findViewById<ViewPager2>(R.id.view_pager)
(requireActivity() as MainActivity).setActionBarButtonFunction(3, R.string.app_settings_title)
val mainActivity = requireActivity() as MainActivity
mainActivity.setActionBarButtonFunction(
ACTION_BAR_BUTTON_INDEX,
R.string.app_settings_title,
)
val callback =
requireActivity().onBackPressedDispatcher.addCallback(this) {
viewpager.setCurrentItem(3, true)
viewpager.setCurrentItem(ACTION_BAR_BUTTON_INDEX, true)
(requireActivity() as MainActivity).supportActionBar?.setDisplayHomeAsUpEnabled(false)
(requireActivity() as MainActivity).setActionBarVisibility(false)
}
Expand All @@ -55,8 +59,8 @@ class LanguageSettingsFragment : Fragment() {
val viewpager = requireActivity().findViewById<ViewPager2>(R.id.view_pager)
val frameLayout = requireActivity().findViewById<ViewGroup>(R.id.fragment_container)
(requireActivity() as MainActivity).setActionBarVisibility(false)
if (viewpager.currentItem == 3) {
viewpager.setCurrentItem(3, true)
if (viewpager.currentItem == ACTION_BAR_BUTTON_INDEX) {
viewpager.setCurrentItem(ACTION_BAR_BUTTON_INDEX, true)
frameLayout.visibility = View.GONE
(requireActivity() as MainActivity).setActionBarVisibility(false)
} else {
Expand Down Expand Up @@ -86,7 +90,13 @@ class LanguageSettingsFragment : Fragment() {
(requireActivity() as MainActivity).setActionBarTitle(titleInt)
(requireActivity() as MainActivity).showFragmentContainer()
(requireActivity() as MainActivity).setActionBarButtonVisibility(true)
(requireActivity() as MainActivity).setActionBarButtonFunction(3, R.string.app_settings_title)
val mainActivity = requireActivity() as MainActivity
val actionBarButtonIndex = ACTION_BAR_BUTTON_INDEX
val titleResId = R.string.app_settings_title
mainActivity.setActionBarButtonFunction(
actionBarButtonIndex,
titleResId,
)
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
(requireActivity() as MainActivity).setActionBarButtonVisibility(false)
parentFragmentManager
Expand Down Expand Up @@ -224,32 +234,7 @@ class LanguageSettingsFragment : Fragment() {
)
}
}
list.add(
SwitchItem(
isChecked = sharedPref.getBoolean("period_on_double_tap_$language", false),
title = getString(R.string.app_settings_keyboard_functionality_double_space_period),
description = getString(R.string.app_settings_keyboard_functionality_double_space_period_description),
action = {
PreferencesHelper.setPeriodOnSpaceBarDoubleTapPreference(requireContext(), language, true)
},
action2 = {
PreferencesHelper.setPeriodOnSpaceBarDoubleTapPreference(requireContext(), language, false)
},
),
)
list.add(
SwitchItem(
isChecked = sharedPref.getBoolean("emoji_suggestions_$language", true),
title = getString(R.string.app_settings_keyboard_functionality_auto_suggest_emoji),
description = getString(R.string.app_settings_keyboard_functionality_auto_suggest_emoji_description),
action = {
PreferencesHelper.setEmojiAutoSuggestionsPreference(requireContext(), language, true)
},
action2 = {
PreferencesHelper.setEmojiAutoSuggestionsPreference(requireContext(), language, false)
},
),
)

list.add(
SwitchItem(
isChecked = sharedPref.getBoolean("period_and_comma_$language", false),
Expand Down Expand Up @@ -279,4 +264,8 @@ class LanguageSettingsFragment : Fragment() {
else -> return R.string.app__global_english
}
}

companion object {
private const val ACTION_BAR_BUTTON_INDEX = 3
}
}
6 changes: 5 additions & 1 deletion app/src/main/java/be/scri/fragments/PrivacyPolicyFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class PrivacyPolicyFragment : Fragment() {

textView?.let {
val params = it.layoutParams as ViewGroup.MarginLayoutParams
params.topMargin = -50
params.topMargin = NEGATIVE_TOP_MARGIN
it.layoutParams = params
}

Expand Down Expand Up @@ -84,4 +84,8 @@ class PrivacyPolicyFragment : Fragment() {
)
return binding.root
}

companion object {
private const val NEGATIVE_TOP_MARGIN = -50
}
}
4 changes: 2 additions & 2 deletions app/src/main/java/be/scri/helpers/AlphanumericComparator.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package be.scri.helpers

// taken from https://gist.github.com/MichaelRocks/1b94bb44c7804e999dbf31dac86955ec
// make IMG_5.jpg come before IMG_10.jpg
// Taken from https://gist.github.com/MichaelRocks/1b94bb44c7804e999dbf31dac86955ec.
// Make IMG_5.jpg come before IMG_10.jpg.
class AlphanumericComparator {
fun compare(
string1: String,
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/be/scri/helpers/CommonsConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package be.scri.helpers

val DARK_GREY = 0xFF333333.toInt()

// shared preferences
// Shared preferences.
const val PREFS_KEY = "Prefs"
const val TEXT_COLOR = "text_color"
const val KEY_COLOR = "key_color"
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/be/scri/helpers/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ const val SHIFT_OFF = 0
const val SHIFT_ON_ONE_CHAR = 1
const val SHIFT_ON_PERMANENT = 2

// limit the count of alternative characters that show up at long pressing a key
// Limit the count of alternative characters that show up at long pressing a key.
const val MAX_KEYS_PER_MINI_ROW = 9

// shared prefs
// Shared preferences.
const val VIBRATE_ON_KEYPRESS = "vibrate_on_keypress"
const val SHOW_POPUP_ON_KEYPRESS = "show_popup_on_keypress"
const val DARK_THEME = "dark_theme"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/be/scri/helpers/DatabaseHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ class DatabaseHelper(
}

override fun onCreate(db: SQLiteDatabase) {
// No operation for now
}

override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int,
) {
// No operation for now
}

fun loadDatabase(language: String) {
Expand Down
7 changes: 5 additions & 2 deletions app/src/main/java/be/scri/helpers/MyKeyboard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class MyKeyboard {
private const val TAG_KEY = "Key"
private const val EDGE_LEFT = 0x01
private const val EDGE_RIGHT = 0x02
private const val WIDTH_DIVIDER = 10
const val KEYCODE_SHIFT = -1
const val KEYCODE_MODE_CHANGE = -2
const val KEYCODE_ENTER = -4
Expand Down Expand Up @@ -302,7 +303,7 @@ class MyKeyboard {
) {
mDisplayWidth = context.resources.displayMetrics.widthPixels
mDefaultHorizontalGap = 0
mDefaultWidth = mDisplayWidth / 10
mDefaultWidth = mDisplayWidth / WIDTH_DIVIDER
mDefaultHeight = mDefaultWidth
mKeys = ArrayList()
mEnterKeyType = enterKeyType
Expand Down Expand Up @@ -455,7 +456,9 @@ class MyKeyboard {
parser: XmlResourceParser,
) {
val a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.MyKeyboard)
mDefaultWidth = getDimensionOrFraction(a, R.styleable.MyKeyboard_keyWidth, mDisplayWidth, mDisplayWidth / 10)
val keyWidthResId = R.styleable.MyKeyboard_keyWidth
val defaultWidth = mDisplayWidth / WIDTH_DIVIDER
mDefaultWidth = getDimensionOrFraction(a, keyWidthResId, mDisplayWidth, defaultWidth)
mDefaultHeight = res.getDimension(R.dimen.key_height).toInt()
mDefaultHorizontalGap = getDimensionOrFraction(a, R.styleable.MyKeyboard_horizontalGap, mDisplayWidth, 0)
a.recycle()
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/be/scri/helpers/RatingHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import android.widget.Toast
import be.scri.activities.MainActivity
import com.google.android.play.core.review.ReviewManagerFactory
Expand All @@ -15,6 +16,7 @@ class RatingHelper {
val packageManager = context.packageManager
packageManager.getInstallerPackageName(context.packageName)
} catch (e: PackageManager.NameNotFoundException) {
Log.e("RatingHelper", "Failed to get install source", e)
null
}

Expand Down
Loading

0 comments on commit e760e14

Please sign in to comment.