First Commit
This commit is contained in:
14
tools/adb/callLinkCustomScheme.sh
Executable file
14
tools/adb/callLinkCustomScheme.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Format is:
|
||||
# element://call?url=some-encoded-url
|
||||
# For instance
|
||||
# element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall
|
||||
|
||||
adb shell am start -a android.intent.action.VIEW -d element://call?url=https%3A%2F%2Fcall.element.io%2FTestElementCall
|
||||
14
tools/adb/callLinkCustomScheme2.sh
Executable file
14
tools/adb/callLinkCustomScheme2.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Format is:
|
||||
# io.element.call:/?url=some-encoded-url
|
||||
# For instance
|
||||
# io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall
|
||||
|
||||
adb shell am start -a android.intent.action.VIEW -d io.element.call:/?url=https%3A%2F%2Fcall.element.io%2FTestElementCall
|
||||
14
tools/adb/callLinkHttps.sh
Executable file
14
tools/adb/callLinkHttps.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Format is:
|
||||
# https://call.element.io/*
|
||||
# For instance
|
||||
# https://call.element.io/TestElementCall
|
||||
|
||||
adb shell am start -a android.intent.action.VIEW -d https://call.element.io/TestElementCall
|
||||
19
tools/adb/deeplink.sh
Executable file
19
tools/adb/deeplink.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Format is:
|
||||
# elementx://open/{sessionId} to open a session
|
||||
# elementx://open/{sessionId}/{roomId} to open a room
|
||||
# elementx://open/{sessionId}/{roomId}/{eventId} to open a thread
|
||||
|
||||
# Open a session
|
||||
# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org
|
||||
# Open a room
|
||||
adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org
|
||||
# Open a thread
|
||||
# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org/\\\$threadId
|
||||
10
tools/adb/deeplink_external.sh
Executable file
10
tools/adb/deeplink_external.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE \
|
||||
-d "https://app.element.io/#/room/!cuqHozLHNBgupgLMKN:matrix.org/%24LZDOueY3R8OD2ZYf8FLKtu95aF7imLBC3F5TIUj-4cc"
|
||||
10
tools/adb/deeplink_matrix.sh
Executable file
10
tools/adb/deeplink_matrix.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-d "matrix:r/element-android:matrix.org"
|
||||
10
tools/adb/deeplink_matrixto.sh
Executable file
10
tools/adb/deeplink_matrixto.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-d "element://room/%23element-android%3Amatrix.org"
|
||||
13
tools/adb/deeplink_mobile.sh
Executable file
13
tools/adb/deeplink_mobile.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Format is:
|
||||
# https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org
|
||||
|
||||
adb shell am start -a android.intent.action.VIEW \
|
||||
-d "https://mobile.element.io/element/?account_provider=element.io\\&login_hint=mxid:@alice:element.io"
|
||||
19
tools/adb/disable_app_standby.sh
Executable file
19
tools/adb/disable_app_standby.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby
|
||||
|
||||
echo " => Standby OFF"
|
||||
|
||||
set -x
|
||||
package_name="io.element.android.x.debug"
|
||||
adb shell dumpsys battery reset
|
||||
adb shell am set-inactive "${package_name}" false
|
||||
adb shell am get-inactive "${package_name}"
|
||||
|
||||
tools/adb/print_device_state.sh
|
||||
17
tools/adb/disable_doze_mode.sh
Executable file
17
tools/adb/disable_doze_mode.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze
|
||||
|
||||
echo " => Disable doze mode"
|
||||
|
||||
set -x
|
||||
adb shell dumpsys deviceidle unforce
|
||||
adb shell dumpsys battery reset
|
||||
|
||||
tools/adb/print_device_state.sh
|
||||
10
tools/adb/disable_talkback.sh
Executable file
10
tools/adb/disable_talkback.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
adb shell settings put secure enabled_accessibility_services null
|
||||
|
||||
19
tools/adb/enable_app_standby.sh
Executable file
19
tools/adb/enable_app_standby.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_your_app_with_app_standby
|
||||
|
||||
echo " => Standby ON"
|
||||
|
||||
set -x
|
||||
package_name="io.element.android.x.debug"
|
||||
adb shell dumpsys battery unplug
|
||||
adb shell am set-inactive "${package_name}" true
|
||||
adb shell am get-inactive "${package_name}"
|
||||
|
||||
tools/adb/print_device_state.sh
|
||||
17
tools/adb/enable_doze_mode.sh
Executable file
17
tools/adb/enable_doze_mode.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze
|
||||
|
||||
echo " => Enable doze mode"
|
||||
|
||||
set -x
|
||||
adb shell dumpsys battery unplug
|
||||
adb shell dumpsys deviceidle force-idle
|
||||
|
||||
tools/adb/print_device_state.sh
|
||||
9
tools/adb/enable_talkback.sh
Executable file
9
tools/adb/enable_talkback.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
adb shell settings put secure enabled_accessibility_services com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService
|
||||
15
tools/adb/oidc.sh
Executable file
15
tools/adb/oidc.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Format is:
|
||||
|
||||
# Error
|
||||
# adb shell am start -a android.intent.action.VIEW -d "io.element.android:/?error=access_denied\\&state=IFF1UETGye2ZA8pO"
|
||||
|
||||
# Success
|
||||
adb shell am start -a android.intent.action.VIEW -d "io.element.android:/?state=IFF1UETGye2ZA8pO\\&code=y6X1GZeqA3xxOWcTeShgv8nkgFJXyzWB"
|
||||
19
tools/adb/print_device_state.sh
Executable file
19
tools/adb/print_device_state.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Ref: https://developer.android.com/training/monitoring-device-state/doze-standby#testing_doze
|
||||
|
||||
echo " => Device state"
|
||||
|
||||
set -x
|
||||
adb shell dumpsys deviceidle get light
|
||||
adb shell dumpsys deviceidle get deep
|
||||
adb shell dumpsys deviceidle get force
|
||||
adb shell dumpsys deviceidle get screen
|
||||
adb shell dumpsys deviceidle get charging
|
||||
adb shell dumpsys deviceidle get network
|
||||
56
tools/check/check_code_quality.sh
Executable file
56
tools/check/check_code_quality.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
#######################################################################################################################
|
||||
# Search forbidden pattern
|
||||
#######################################################################################################################
|
||||
|
||||
searchForbiddenStringsScript=./tmp/search_forbidden_strings.pl
|
||||
|
||||
if [[ -f ${searchForbiddenStringsScript} ]]; then
|
||||
echo "${searchForbiddenStringsScript} already there"
|
||||
else
|
||||
mkdir tmp
|
||||
echo "Get the script"
|
||||
wget https://raw.githubusercontent.com/matrix-org/matrix-dev-tools/develop/bin/search_forbidden_strings.pl -O ${searchForbiddenStringsScript}
|
||||
fi
|
||||
|
||||
if [[ -x ${searchForbiddenStringsScript} ]]; then
|
||||
echo "${searchForbiddenStringsScript} is already executable"
|
||||
else
|
||||
echo "Make the script executable"
|
||||
chmod u+x ${searchForbiddenStringsScript}
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Search for forbidden patterns in Kotlin source files..."
|
||||
|
||||
# list all Kotlin folders of the project.
|
||||
allKotlinDirs=$(find . -type d |grep -v build |grep -v \.git |grep -v \.gradle |grep kotlin$)
|
||||
|
||||
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_code.txt "$allKotlinDirs"
|
||||
|
||||
resultForbiddenStringInCode=$?
|
||||
|
||||
echo
|
||||
echo "Search for forbidden patterns in XML resource files..."
|
||||
|
||||
# list all res folders of the project.
|
||||
allResDirs=$(find . -type d |grep -v build |grep -v \.git |grep -v \.gradle |grep /res$)
|
||||
|
||||
${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_xml.txt "$allResDirs"
|
||||
|
||||
resultForbiddenStringInXml=$?
|
||||
|
||||
if [[ ${resultForbiddenStringInCode} -eq 0 ]] \
|
||||
&& [[ ${resultForbiddenStringInXml} -eq 0 ]]; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "❌ ERROR, please check the logs above."
|
||||
exit 1
|
||||
fi
|
||||
128
tools/check/forbidden_strings_in_code.txt
Executable file
128
tools/check/forbidden_strings_in_code.txt
Executable file
@@ -0,0 +1,128 @@
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# This file list String which are not allowed in source code.
|
||||
# Use Perl regex to write forbidden strings
|
||||
# Note: line cannot start with a space. Use \s instead.
|
||||
# It is possible to specify an authorized number of occurrence with === suffix. Default is 0
|
||||
# Example:
|
||||
# AuthorizedStringThreeTimes===3
|
||||
|
||||
# Extension:kt
|
||||
|
||||
### No import static: use full class name
|
||||
import static
|
||||
|
||||
### Rubbish from merge. Please delete those lines (sometimes in comment)
|
||||
<<<<<<<
|
||||
>>>>>>>
|
||||
|
||||
### carry return before "}". Please remove empty lines.
|
||||
\n\s*\n\s*\}
|
||||
|
||||
### typo detected.
|
||||
formated
|
||||
abtract
|
||||
Succes[^s]
|
||||
succes[^s]
|
||||
|
||||
### Use int instead of Integer
|
||||
protected Integer
|
||||
|
||||
### Use the interface declaration. Example: use type "Map" instead of type "HashMap" to declare variable or parameter. For Kotlin, use mapOf, setOf, ...
|
||||
(private|public|protected| ) (static )?(final )?(HashMap|HashSet|ArrayList)<
|
||||
|
||||
### Use int instead of short
|
||||
Short\.parseShort
|
||||
\(short\)
|
||||
private short
|
||||
final short
|
||||
|
||||
### Line length is limited to 160 chars. Please split long lines
|
||||
#[^─]{161}
|
||||
|
||||
### "DO NOT COMMIT" has been committed
|
||||
DO NOT COMMIT
|
||||
|
||||
### invalid formatting
|
||||
\s{8}/\*\n \*
|
||||
# Now checked by ktlint
|
||||
# [^\w]if\(
|
||||
# while\(
|
||||
# for\(
|
||||
|
||||
# Add space after //
|
||||
# DISABLED To re-enable when code will be formatted globally
|
||||
#^\s*//[^\s]
|
||||
|
||||
### invalid formatting (too many space char)
|
||||
^ /\*
|
||||
|
||||
### unnecessary parenthesis around numbers, example: " (0)"
|
||||
\(\d+\)[^"]
|
||||
|
||||
### import the package, do not use long class name with package
|
||||
android\.os\.Build\.
|
||||
|
||||
### Tab char is forbidden. Use only spaces
|
||||
\t
|
||||
|
||||
# Empty lines and trailing space
|
||||
# DISABLED To re-enable when code will be formatted globally
|
||||
#[ ]$
|
||||
|
||||
### Deprecated, use retrofit2.HttpException
|
||||
import retrofit2\.adapter\.rxjava\.HttpException
|
||||
|
||||
### This is generally not necessary, no need to reset the padding if there is no drawable
|
||||
setCompoundDrawablePadding\(0\)
|
||||
|
||||
# Change thread with Rx
|
||||
# DISABLED
|
||||
#runOnUiThread
|
||||
|
||||
### Bad formatting of chain (missing new line)
|
||||
\w\.flatMap\(
|
||||
|
||||
### In Kotlin, Void has to be null safe, i.e. use 'Void?' instead of 'Void'
|
||||
\: Void\)
|
||||
|
||||
### Kotlin conversion tools introduce this, but is can be replace by trim()
|
||||
trim \{ it \<\= \' \' \}
|
||||
|
||||
### Put the operator at the beginning of next line
|
||||
==$
|
||||
|
||||
### Use JsonUtils.getBasicGson()
|
||||
new Gson\(\)
|
||||
|
||||
### Use TextUtils.formatFileSize
|
||||
Formatter\.formatFileSize===1
|
||||
|
||||
### Use TextUtils.formatFileSize with short format param to true
|
||||
Formatter\.formatShortFileSize===1
|
||||
|
||||
### Use `Context#getSystemService` extension function provided by `core-ktx`
|
||||
getSystemService\(Context
|
||||
|
||||
### Use DefaultSharedPreferences.getInstance() instead for better performance
|
||||
PreferenceManager\.getDefaultSharedPreferences==2
|
||||
|
||||
### Use the Clock interface, or use `measureTimeMillis`
|
||||
System\.currentTimeMillis\(\)===1
|
||||
|
||||
### Remove extra space between the name and the description
|
||||
\* @\w+ \w+ +
|
||||
|
||||
### Suspicious String template. Please check that the string template will behave as expected, i.e. the class field and not the whole object will be used. For instance `Timber.d("$event.type")` is not correct, you should write `Timber.d("${event.type}")`. In the former the whole event content will be logged, since it's a data class.
|
||||
Timber.*\$[a-zA-Z_]\w*\??\.[a-zA-Z_]
|
||||
|
||||
### Use `import io.element.android.libraries.ui.strings.CommonStrings` then `CommonStrings.<stringKey>` instead
|
||||
import io\.element\.android\.libraries\.ui\.strings\.R
|
||||
|
||||
# Accessibility
|
||||
### Use string resource for `contentDescription`, or null instead of empty string
|
||||
contentDescription = "
|
||||
28
tools/check/forbidden_strings_in_xml.txt
Executable file
28
tools/check/forbidden_strings_in_xml.txt
Executable file
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# This file list String which are not allowed in resource.
|
||||
# Use Perl regex to write forbidden strings
|
||||
# Note: line cannot start with a space. Use \s instead.
|
||||
# It is possible to specify an authorized number of occurrence with === suffix. Default is 0
|
||||
# Example:
|
||||
# AuthorizedStringThreeTimes===3
|
||||
|
||||
# Extension:xml
|
||||
|
||||
### Empty tag detected. Empty translation or plurals?
|
||||
"></
|
||||
">""</
|
||||
|
||||
### Rubbish from merge. Please delete those lines (sometimes in comment)
|
||||
<<<<<<<
|
||||
>>>>>>>
|
||||
|
||||
### "DO NOT COMMIT" has been committed
|
||||
DO NOT COMMIT
|
||||
|
||||
### Tab char is forbidden. Use only spaces
|
||||
\t
|
||||
24
tools/compose/check_stability.sh
Executable file
24
tools/compose/check_stability.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
set -e
|
||||
|
||||
# Build the project with compose report
|
||||
echo "Building the project with compose report..."
|
||||
./gradlew assembleGplayDebug -PcomposeCompilerReports=true -PcomposeCompilerMetrics=true --stacktrace
|
||||
|
||||
echo "Checking stability of State classes..."
|
||||
# Using the find command, list all the files ending with -classes.txt
|
||||
find . -type f -name "*-classes.txt" | while read -r file; do
|
||||
# echo "Processing $file"
|
||||
# Check that there is no line containing "unstable class .*State {"
|
||||
if grep -E 'unstable class .*State \{' "$file"; then
|
||||
echo "❌ ERROR: Found unstable State class in $file"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
125
tools/compound/addAutoMirrored.py
Normal file
125
tools/compound/addAutoMirrored.py
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
files = [
|
||||
"ic_compound_arrow_left.xml",
|
||||
"ic_compound_arrow_right.xml",
|
||||
"ic_compound_arrow_up_right.xml",
|
||||
"ic_compound_attachment.xml",
|
||||
"ic_compound_block.xml",
|
||||
"ic_compound_chart.xml",
|
||||
"ic_compound_chat.xml",
|
||||
"ic_compound_chat_new.xml",
|
||||
"ic_compound_chat_problem.xml",
|
||||
"ic_compound_chat_solid.xml",
|
||||
"ic_compound_chevron_left.xml",
|
||||
"ic_compound_chevron_right.xml",
|
||||
"ic_compound_cloud.xml",
|
||||
"ic_compound_cloud_solid.xml",
|
||||
"ic_compound_collapse.xml",
|
||||
"ic_compound_company.xml",
|
||||
"ic_compound_compose.xml",
|
||||
"ic_compound_copy.xml",
|
||||
"ic_compound_dark_mode.xml",
|
||||
"ic_compound_devices.xml",
|
||||
"ic_compound_document.xml",
|
||||
"ic_compound_earpiece.xml",
|
||||
"ic_compound_edit.xml",
|
||||
"ic_compound_edit_solid.xml",
|
||||
"ic_compound_expand.xml",
|
||||
"ic_compound_extensions.xml",
|
||||
"ic_compound_extensions_solid.xml",
|
||||
"ic_compound_file_error.xml",
|
||||
"ic_compound_files.xml",
|
||||
"ic_compound_forward.xml",
|
||||
"ic_compound_guest.xml",
|
||||
"ic_compound_history.xml",
|
||||
"ic_compound_host.xml",
|
||||
"ic_compound_image.xml",
|
||||
"ic_compound_image_error.xml",
|
||||
"ic_compound_indent_decrease.xml",
|
||||
"ic_compound_indent_increase.xml",
|
||||
"ic_compound_italic.xml",
|
||||
"ic_compound_key.xml",
|
||||
"ic_compound_key_off.xml",
|
||||
"ic_compound_key_off_solid.xml",
|
||||
"ic_compound_key_solid.xml",
|
||||
"ic_compound_leave.xml",
|
||||
"ic_compound_link.xml",
|
||||
"ic_compound_list_bulleted.xml",
|
||||
"ic_compound_lock_off.xml",
|
||||
"ic_compound_mark_as_unread.xml",
|
||||
"ic_compound_mark_threads_as_read.xml",
|
||||
"ic_compound_marker_read_receipts.xml",
|
||||
"ic_compound_mic_off.xml",
|
||||
"ic_compound_mic_off_solid.xml",
|
||||
"ic_compound_notifications_off.xml",
|
||||
"ic_compound_notifications_off_solid.xml",
|
||||
"ic_compound_offline.xml",
|
||||
"ic_compound_play.xml",
|
||||
"ic_compound_play_solid.xml",
|
||||
"ic_compound_polls.xml",
|
||||
"ic_compound_polls_end.xml",
|
||||
"ic_compound_pop_out.xml",
|
||||
"ic_compound_qr_code.xml",
|
||||
"ic_compound_quote.xml",
|
||||
"ic_compound_reaction_add.xml",
|
||||
"ic_compound_reply.xml",
|
||||
"ic_compound_restart.xml",
|
||||
"ic_compound_room.xml",
|
||||
"ic_compound_search.xml",
|
||||
"ic_compound_send.xml",
|
||||
"ic_compound_send_solid.xml",
|
||||
"ic_compound_share_android.xml",
|
||||
"ic_compound_sidebar.xml",
|
||||
"ic_compound_sign_out.xml",
|
||||
"ic_compound_spinner.xml",
|
||||
"ic_compound_spotlight.xml",
|
||||
"ic_compound_switch_camera_solid.xml",
|
||||
"ic_compound_threads.xml",
|
||||
"ic_compound_threads_solid.xml",
|
||||
"ic_compound_unknown.xml",
|
||||
"ic_compound_unknown_solid.xml",
|
||||
"ic_compound_unpin.xml",
|
||||
"ic_compound_user_add.xml",
|
||||
"ic_compound_user_add_solid.xml",
|
||||
"ic_compound_video_call.xml",
|
||||
"ic_compound_video_call_declined_solid.xml",
|
||||
"ic_compound_video_call_missed_solid.xml",
|
||||
"ic_compound_video_call_off.xml",
|
||||
"ic_compound_video_call_off_solid.xml",
|
||||
"ic_compound_video_call_solid.xml",
|
||||
"ic_compound_visibility_off.xml",
|
||||
"ic_compound_voice_call.xml",
|
||||
"ic_compound_voice_call_solid.xml",
|
||||
"ic_compound_volume_off.xml",
|
||||
"ic_compound_volume_off_solid.xml",
|
||||
"ic_compound_volume_on.xml",
|
||||
"ic_compound_volume_on_solid.xml",
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
for file in files:
|
||||
# Open file for read
|
||||
with open("./libraries/compound/src/main/res/drawable/" + file, 'r') as f:
|
||||
data = f.read().split("\n")
|
||||
# Open file to write
|
||||
with open("./libraries/compound/src/main/res/drawable/" + file, 'w') as f:
|
||||
# Write new data
|
||||
# write the 3 first lines in data
|
||||
for i in range(3):
|
||||
f.write(data[i] + "\n")
|
||||
f.write(" android:autoMirrored=\"true\"\n")
|
||||
# write the rest of the data
|
||||
for i in range(3, len(data) - 1):
|
||||
f.write(data[i] + "\n")
|
||||
print("Added autoMirrored to " + str(len(files)) + " files.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
45
tools/compound/import_tokens.sh
Executable file
45
tools/compound/import_tokens.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
set -e
|
||||
|
||||
BRANCH='main'
|
||||
|
||||
while getopts b: flag
|
||||
do
|
||||
case "${flag}" in
|
||||
b) BRANCH=${OPTARG};;
|
||||
*) echo "usage: $0 [-b branch]" >&2
|
||||
exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Branch used: $BRANCH"
|
||||
|
||||
echo "Cloning the compound-design-tokens repository..."
|
||||
if [ -d tmpCompound ]; then
|
||||
echo "Deleting tmpCompound folder..."
|
||||
rm -rf tmpCompound
|
||||
fi
|
||||
mkdir tmpCompound
|
||||
pushd tmpCompound
|
||||
git clone --branch "${BRANCH}" https://github.com/element-hq/compound-design-tokens
|
||||
|
||||
echo "Copying files from tokens repository..."
|
||||
rm -R ../libraries/compound/src/main/res/drawable
|
||||
cp -R compound-design-tokens/assets/android/res/drawable ../libraries/compound/src/main/res/
|
||||
cp -R compound-design-tokens/assets/android/src/* ../libraries/compound/src/main/kotlin/io/element/android/compound/tokens/generated/
|
||||
cp -R compound-design-tokens/assets/android/res/theme.iife.js ../libraries/compound/src/main/assets/theme.iife.js
|
||||
popd
|
||||
|
||||
echo "Adding autoMirrored attribute..."
|
||||
python3 ./tools/compound/addAutoMirrored.py
|
||||
|
||||
echo "Removing temporary files..."
|
||||
rm -rf tmpCompound
|
||||
|
||||
echo "Done!"
|
||||
41
tools/danger/dangerfile-lint.js
Normal file
41
tools/danger/dangerfile-lint.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { schedule } from 'danger'
|
||||
|
||||
/**
|
||||
* Ref and documentation: https://github.com/damian-burke/danger-plugin-lint-report
|
||||
* This file will check all the error in XML Checkstyle format.
|
||||
* It covers, lint, ktlint, and detekt errors
|
||||
*/
|
||||
|
||||
const reporter = require("danger-plugin-lint-report")
|
||||
schedule(reporter.scan({
|
||||
/**
|
||||
* File mask used to find XML checkstyle reports.
|
||||
*/
|
||||
fileMask: "**/reports/**/**.xml",
|
||||
/**
|
||||
* If set to true, the severity will be used to switch between the different message formats (message, warn, fail).
|
||||
*/
|
||||
reportSeverity: true,
|
||||
/**
|
||||
* If set to true, only issues will be reported that are contained in the current changeset (line comparison).
|
||||
* If set to false, all issues that are in modified files will be reported.
|
||||
*/
|
||||
requireLineModification: false,
|
||||
/**
|
||||
* Optional: Sets a prefix foreach violation message.
|
||||
* This can be useful if there are multiple reports being parsed to make them distinguishable.
|
||||
*/
|
||||
// outputPrefix?: ""
|
||||
|
||||
/**
|
||||
* Optional: If set to true, it will remove duplicate violations.
|
||||
*/
|
||||
removeDuplicates: true,
|
||||
}))
|
||||
121
tools/danger/dangerfile.js
Normal file
121
tools/danger/dangerfile.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
const {danger, warn} = require('danger')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
/**
|
||||
* Note: if you update the checks in this file, please also update the file ./docs/danger.md
|
||||
*/
|
||||
|
||||
// Useful to see what we got in danger object
|
||||
// warn(JSON.stringify(danger))
|
||||
|
||||
const pr = danger.github.pr
|
||||
const github = danger.github
|
||||
// User who has created the PR.
|
||||
const user = pr.user.login
|
||||
const modified = danger.git.modified_files
|
||||
const created = danger.git.created_files
|
||||
const editedFiles = [...modified, ...created]
|
||||
|
||||
// Check that the PR has a description
|
||||
if (pr.body.length == 0) {
|
||||
warn("Please provide a description for this PR.")
|
||||
}
|
||||
|
||||
// Warn when there is a big PR
|
||||
if (editedFiles.length > 50) {
|
||||
message("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.")
|
||||
}
|
||||
|
||||
// Request a correct title for each PR
|
||||
if (pr.title.endsWith("…")) {
|
||||
fail("Please provide a complete title that can be used as a changelog entry.")
|
||||
}
|
||||
|
||||
// Request a `PR-` label for each PR
|
||||
if (pr.labels.filter((label) => label.name.startsWith("PR-")).length != 1) {
|
||||
fail("Please add a `PR-` label to categorise the changelog entry.")
|
||||
}
|
||||
|
||||
// check that frozen classes have not been modified
|
||||
const frozenClasses = [
|
||||
]
|
||||
|
||||
frozenClasses.forEach(frozen => {
|
||||
if (editedFiles.some(file => file.endsWith(frozen))) {
|
||||
fail("Frozen class `" + frozen + "` has been modified. Please do not modify frozen class.")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const previewAnnotations = [
|
||||
'androidx.compose.ui.tooling.preview.Preview',
|
||||
'io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight',
|
||||
'io.element.android.libraries.designsystem.preview.PreviewsDayNight'
|
||||
]
|
||||
|
||||
const filesWithPreviews = editedFiles.filter(file => file.endsWith(".kt")).filter(file => {
|
||||
const content = fs.readFileSync(file);
|
||||
return previewAnnotations.some((ann) => content.includes("import " + ann));
|
||||
})
|
||||
|
||||
const composablePreviewProviderContents = fs.readFileSync('tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt');
|
||||
const packageTreesRegex = /private val PACKAGE_TREES = arrayOf\(([\w\W]+?)\n\)/gm;
|
||||
const packageTreesMatch = packageTreesRegex.exec(composablePreviewProviderContents)[1];
|
||||
const scannedPreviewPackageTrees = packageTreesMatch
|
||||
.replaceAll("\"", "")
|
||||
.replaceAll(",", "")
|
||||
.split('\n').map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
|
||||
const previewPackagesNotIncludedInScreenshotTests = filesWithPreviews.map((file) => {
|
||||
const content = fs.readFileSync(file);
|
||||
const packageRegex = /package\s+([a-zA-Z0-9.]+)/;
|
||||
const packageMatch = packageRegex.exec(content);
|
||||
|
||||
if (!packageMatch || packageMatch.length != 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return packageMatch[1];
|
||||
|
||||
|
||||
}).filter((package) => {
|
||||
if (!package) {
|
||||
return false;
|
||||
}
|
||||
if (!scannedPreviewPackageTrees.some((prefix) => package.includes(prefix))) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (previewPackagesNotIncludedInScreenshotTests.length > 0) {
|
||||
const packagesList = previewPackagesNotIncludedInScreenshotTests.map((p) => '- `' + p + '`').join("\n");
|
||||
warn("You have made changes to a file containing a `@Preview` annotated function but its package name prefix is not included in the `ComposablePreviewProvider`.\nPackages missing in `tests/uitests/src/test/kotlin/base/ComposablePreviewProvider.kt`: \n" + packagesList);
|
||||
}
|
||||
|
||||
// Check for pngs on resources
|
||||
const hasPngs = editedFiles.filter(file => {
|
||||
file.toLowerCase().endsWith(".png") && !file.includes("snapshots/images/") // Exclude screenshots
|
||||
}).length > 0
|
||||
if (hasPngs) {
|
||||
warn("You seem to have made changes to some images. Please consider using an vector drawable.")
|
||||
}
|
||||
|
||||
// Check that translations have not been modified by developers
|
||||
const translationAllowList = [
|
||||
"ElementBot",
|
||||
]
|
||||
|
||||
if (!translationAllowList.includes(user)) {
|
||||
if (editedFiles.some(file => file.endsWith("translations.xml"))) {
|
||||
fail("Some translation files have been edited. Only user `ElementBot` (i.e. translations coming from Localazy) is allowed to do that.\nPlease read more about translations management [in the doc](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#strings).")
|
||||
}
|
||||
}
|
||||
88
tools/dependencies/checkDependencies.py
Executable file
88
tools/dependencies/checkDependencies.py
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def getProjectDependencies():
|
||||
print("=> Computing dependencies...")
|
||||
command = subprocess.run(
|
||||
["./gradlew :app:dependencies"],
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
data = command.stdout
|
||||
# Remove the trailing info like "(*)"
|
||||
result = list(map(lambda x: x.split(" (")[0], data.split("\n")))
|
||||
# Filter out comment line
|
||||
result = list(filter(lambda x: "--- project" in x, result))
|
||||
return result
|
||||
|
||||
|
||||
def checkThatModulesExist(dependencies):
|
||||
print("=> Checking that all modules exist...")
|
||||
error = 0
|
||||
modules = set()
|
||||
for line in dependencies:
|
||||
if line:
|
||||
line = line.split(" ")
|
||||
for elem in line:
|
||||
if ":" in elem:
|
||||
modules.add(elem)
|
||||
for module in modules:
|
||||
path = "." + module.replace(":", "/") + "/build.gradle.kts"
|
||||
if not os.path.exists(path):
|
||||
error += 1
|
||||
print("Error: there is at least one dependency to '" + module + "' but the module does not exist.")
|
||||
print(" Please remove occurrence(s) of 'implementation(projects" + module.replace(":", ".") + ")'.")
|
||||
return error
|
||||
|
||||
|
||||
def checkThatThereIsNoTestDependency(dependencies):
|
||||
print("=> Checking that there are no test dependencies...")
|
||||
errors = set()
|
||||
currentProject = ""
|
||||
for line in dependencies:
|
||||
if line.startswith("+--- project "):
|
||||
currentProject = line.split(" ")[2]
|
||||
else:
|
||||
if ":test" in currentProject:
|
||||
continue
|
||||
else:
|
||||
subProject = line.split(" ")[-1]
|
||||
if subProject.endswith(":test") or ":tests:" in subProject and "detekt-rules" not in subProject:
|
||||
error = "Error: '" + currentProject + "' depends on the test project '" + subProject + "'\n"
|
||||
error += " Please replace occurrence(s) of 'implementation(projects" + subProject.replace(":", ".") + ")'"
|
||||
error += " with 'testImplementation(projects" + subProject.replace(":", ".") + ")'."
|
||||
errors.add(error)
|
||||
for error in errors:
|
||||
print(error)
|
||||
return len(errors)
|
||||
|
||||
|
||||
def main():
|
||||
dependencies = getProjectDependencies()
|
||||
# for dep in dependencies:
|
||||
# print(dep)
|
||||
errors = 0
|
||||
errors += checkThatModulesExist(dependencies)
|
||||
errors += checkThatThereIsNoTestDependency(dependencies)
|
||||
print()
|
||||
if (errors == 0):
|
||||
print("All checks passed successfully.")
|
||||
elif (errors == 1):
|
||||
print("Please fix the error above.")
|
||||
else:
|
||||
print("Please fix the " + str(errors) + " errors above.")
|
||||
exit(errors)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
275
tools/detekt/detekt.yml
Normal file
275
tools/detekt/detekt.yml
Normal file
@@ -0,0 +1,275 @@
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Default rules: https://github.com/detekt/detekt/blob/main/detekt-core/src/main/resources/default-detekt-config.yml
|
||||
|
||||
style:
|
||||
AlsoCouldBeApply:
|
||||
active: true
|
||||
BracesOnWhenStatements:
|
||||
active: false
|
||||
CascadingCallWrapping:
|
||||
active: true
|
||||
includeElvis: true
|
||||
DataClassShouldBeImmutable:
|
||||
active: true
|
||||
EqualsNullCall:
|
||||
active: true
|
||||
EqualsOnSignatureLine:
|
||||
active: true
|
||||
ExplicitCollectionElementAccessMethod:
|
||||
active: true
|
||||
ExplicitItLambdaParameter:
|
||||
active: true
|
||||
MaxLineLength:
|
||||
# Default is 120
|
||||
maxLineLength: 160
|
||||
MagicNumber:
|
||||
active: false
|
||||
ReturnCount:
|
||||
active: false
|
||||
UnnecessaryAbstractClass:
|
||||
active: true
|
||||
FunctionOnlyReturningConstant:
|
||||
active: false
|
||||
UnusedPrivateMember:
|
||||
active: true
|
||||
DestructuringDeclarationWithTooManyEntries:
|
||||
active: true
|
||||
maxDestructuringEntries: 5
|
||||
UnusedParameter:
|
||||
active: true
|
||||
UnnecessaryInnerClass:
|
||||
active: true
|
||||
UnnecessaryLet:
|
||||
active: true
|
||||
UnnecessaryParentheses:
|
||||
active: true
|
||||
allowForUnclearPrecedence: false
|
||||
UntilInsteadOfRangeTo:
|
||||
active: true
|
||||
UnusedImports:
|
||||
active: true
|
||||
UnusedPrivateProperty:
|
||||
active: true
|
||||
ThrowsCount:
|
||||
active: false
|
||||
LoopWithTooManyJumpStatements:
|
||||
active: true
|
||||
SerialVersionUIDInSerializableClass:
|
||||
active: false
|
||||
ProtectedMemberInFinalClass:
|
||||
active: true
|
||||
UseCheckOrError:
|
||||
active: true
|
||||
OptionalUnit:
|
||||
active: true
|
||||
PreferToOverPairSyntax:
|
||||
active: true
|
||||
RedundantExplicitType:
|
||||
active: true
|
||||
TrailingWhitespace:
|
||||
active: true
|
||||
TrimMultilineRawString:
|
||||
active: true
|
||||
trimmingMethods:
|
||||
- 'trimIndent'
|
||||
- 'trimMargin'
|
||||
UnderscoresInNumericLiterals:
|
||||
active: true
|
||||
acceptableLength: 4
|
||||
allowNonStandardGrouping: false
|
||||
UnnecessaryAnnotationUseSiteTarget:
|
||||
active: true
|
||||
UnnecessaryBackticks:
|
||||
active: true
|
||||
UnnecessaryBracesAroundTrailingLambda:
|
||||
active: true
|
||||
UseDataClass:
|
||||
active: true
|
||||
allowVars: false
|
||||
UseEmptyCounterpart:
|
||||
active: true
|
||||
UseIfEmptyOrIfBlank:
|
||||
active: true
|
||||
UseLet:
|
||||
active: true
|
||||
UseSumOfInsteadOfFlatMapSize:
|
||||
active: true
|
||||
|
||||
coroutines:
|
||||
GlobalCoroutineUsage:
|
||||
# Keep false for now.
|
||||
active: false
|
||||
SuspendFunSwallowedCancellation:
|
||||
active: true
|
||||
SuspendFunWithCoroutineScopeReceiver:
|
||||
active: true
|
||||
|
||||
empty-blocks:
|
||||
EmptyFunctionBlock:
|
||||
active: false
|
||||
EmptySecondaryConstructor:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
ImplicitDefaultLocale:
|
||||
active: true
|
||||
CastNullableToNonNullableType:
|
||||
active: true
|
||||
CastToNullableType:
|
||||
active: true
|
||||
Deprecation:
|
||||
active: true
|
||||
DontDowncastCollectionTypes:
|
||||
active: true
|
||||
ElseCaseInsteadOfExhaustiveWhen:
|
||||
active: true
|
||||
ExitOutsideMain:
|
||||
active: true
|
||||
ImplicitUnitReturnType:
|
||||
active: true
|
||||
allowExplicitReturnType: false
|
||||
MissingPackageDeclaration:
|
||||
active: true
|
||||
excludes: [ '**/*.kts' ]
|
||||
NullCheckOnMutableProperty:
|
||||
active: true
|
||||
NullableToStringCall:
|
||||
active: true
|
||||
PropertyUsedBeforeDeclaration:
|
||||
active: true
|
||||
UnconditionalJumpStatementInLoop:
|
||||
active: true
|
||||
UnnecessaryNotNullCheck:
|
||||
active: true
|
||||
|
||||
exceptions:
|
||||
TooGenericExceptionCaught:
|
||||
active: false
|
||||
SwallowedException:
|
||||
active: false
|
||||
ThrowingExceptionsWithoutMessageOrCause:
|
||||
active: true
|
||||
TooGenericExceptionThrown:
|
||||
active: true
|
||||
InstanceOfCheckForException:
|
||||
active: true
|
||||
ObjectExtendsThrowable:
|
||||
active: true
|
||||
|
||||
complexity:
|
||||
TooManyFunctions:
|
||||
active: false
|
||||
LongMethod:
|
||||
active: false
|
||||
LongParameterList:
|
||||
active: false
|
||||
CyclomaticComplexMethod:
|
||||
active: false
|
||||
NestedBlockDepth:
|
||||
active: false
|
||||
ComplexCondition:
|
||||
active: true
|
||||
LargeClass:
|
||||
active: true
|
||||
|
||||
naming:
|
||||
VariableNaming:
|
||||
active: true
|
||||
TopLevelPropertyNaming:
|
||||
active: true
|
||||
FunctionNaming:
|
||||
active: true
|
||||
ignoreAnnotated: [ 'Composable' ]
|
||||
LambdaParameterNaming:
|
||||
active: true
|
||||
NonBooleanPropertyPrefixedWithIs:
|
||||
active: true
|
||||
VariableMaxLength:
|
||||
active: true
|
||||
|
||||
performance:
|
||||
SpreadOperator:
|
||||
active: false
|
||||
CouldBeSequence:
|
||||
active: true
|
||||
UnnecessaryPartOfBinaryExpression:
|
||||
active: true
|
||||
|
||||
# Note: all rules for `comments` are disabled by default, but I put them here to be aware of their existence
|
||||
comments:
|
||||
AbsentOrWrongFileLicense:
|
||||
active: false
|
||||
CommentOverPrivateFunction:
|
||||
active: false
|
||||
CommentOverPrivateProperty:
|
||||
active: false
|
||||
DeprecatedBlockTag:
|
||||
active: true
|
||||
EndOfSentenceFormat:
|
||||
active: true
|
||||
OutdatedDocumentation:
|
||||
active: true
|
||||
allowParamOnConstructorProperties: true
|
||||
UndocumentedPublicClass:
|
||||
active: false
|
||||
UndocumentedPublicFunction:
|
||||
active: false
|
||||
UndocumentedPublicProperty:
|
||||
active: false
|
||||
|
||||
Compose:
|
||||
CompositionLocalAllowlist:
|
||||
active: true
|
||||
# You can optionally define a list of CompositionLocals that are allowed here
|
||||
allowedCompositionLocals:
|
||||
- LocalCompoundColors
|
||||
- LocalSnackbarDispatcher
|
||||
- LocalCameraPositionState
|
||||
- LocalMediaItemPresenterFactories
|
||||
- LocalTimelineItemPresenterFactories
|
||||
- LocalRoomMemberProfilesCache
|
||||
- LocalMentionSpanUpdater
|
||||
- LocalAnalyticsService
|
||||
- LocalBuildMeta
|
||||
- LocalUiTestMode
|
||||
- LocalSdkIntVersionProvider
|
||||
CompositionLocalNaming:
|
||||
active: true
|
||||
ContentEmitterReturningValues:
|
||||
active: true
|
||||
# You can optionally add your own composables here
|
||||
# contentEmitters: MyComposable,MyOtherComposable
|
||||
ModifierComposable:
|
||||
active: true
|
||||
ModifierMissing:
|
||||
active: true
|
||||
ModifierReused:
|
||||
active: true
|
||||
ModifierWithoutDefault:
|
||||
active: true
|
||||
MultipleEmitters:
|
||||
active: true
|
||||
# You can optionally add your own composables here
|
||||
# contentEmitters: MyComposable,MyOtherComposable
|
||||
MutableParams:
|
||||
active: true
|
||||
ComposableNaming:
|
||||
active: true
|
||||
# You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters)
|
||||
# allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter
|
||||
ComposableParamOrder:
|
||||
active: true
|
||||
PreviewAnnotationNaming:
|
||||
active: true
|
||||
PreviewPublic:
|
||||
active: true
|
||||
# You can optionally disable that only previews with @PreviewParameter are flagged
|
||||
previewPublicOnlyIfParams: false
|
||||
RememberMissing:
|
||||
active: true
|
||||
UnstableCollections:
|
||||
active: true
|
||||
13
tools/docs/generateModuleGraph.sh
Executable file
13
tools/docs/generateModuleGraph.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2022-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
## Dependency graph https://github.com/savvasdalkitsis/module-dependency-graph
|
||||
dotPath=$(pwd)/docs/images/module_graph.dot
|
||||
pngPath=$(pwd)/docs/images/module_graph.png
|
||||
./gradlew graphModules -PdotFilePath="${dotPath}" -PgraphOutputFilePath="${pngPath}" -PautoOpenGraph=false
|
||||
rm "${dotPath}"
|
||||
20
tools/git/validate_lfs.sh
Executable file
20
tools/git/validate_lfs.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2022-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Based on https://cashapp.github.io/paparazzi/#git-lfs
|
||||
|
||||
# Compare the output of `git ls-files ':(attr:filter=lfs)'` against `git lfs ls-files`
|
||||
# If there's no diff we assume the files have been committed using git lfs
|
||||
diff <(git ls-files ':(attr:filter=lfs)' | sort) <(git lfs ls-files -n | sort) >/dev/null
|
||||
|
||||
ret=$?
|
||||
if [[ $ret -ne 0 ]]; then
|
||||
echo >&2 "Detected files committed without using Git LFS."
|
||||
echo >&2 "Install git lfs (eg brew install git-lfs) and run 'git lfs install --local' within the root repository directory and re-commit your files."
|
||||
exit 1
|
||||
fi
|
||||
10
tools/gitflow/gitflow-init.sh
Executable file
10
tools/gitflow/gitflow-init.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
git flow init -d
|
||||
git config gitflow.prefix.versiontag v
|
||||
121
tools/github/download_all_github_artifacts.py
Executable file
121
tools/github/download_all_github_artifacts.py
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2022-2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
# Run `pip3 install requests --break-system-packages` if not installed yet
|
||||
import requests
|
||||
# Run `pip3 install re` if not installed yet
|
||||
import re
|
||||
import time
|
||||
|
||||
# This script downloads artifacts from GitHub.
|
||||
# Ref: https://docs.github.com/en/rest/actions/artifacts#get-an-artifact
|
||||
|
||||
error = False
|
||||
|
||||
### Arguments
|
||||
|
||||
parser = argparse.ArgumentParser(description='Download artifacts from GitHub.')
|
||||
parser.add_argument('-t',
|
||||
'--token',
|
||||
required=True,
|
||||
help='The GitHub token with read access.')
|
||||
parser.add_argument('-r',
|
||||
'--runUrl',
|
||||
required=True,
|
||||
help='the GitHub action run url.')
|
||||
parser.add_argument('-d',
|
||||
'--directory',
|
||||
default="",
|
||||
help='the target directory, where files will be downloaded. If not provided the runId will be used to create a directory.')
|
||||
parser.add_argument('-v',
|
||||
'--verbose',
|
||||
help="increase output verbosity.",
|
||||
action="store_true")
|
||||
parser.add_argument('-s',
|
||||
'--simulate',
|
||||
help="simulate action, do not create folder or download any file.",
|
||||
action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
print("Argument:")
|
||||
print(args)
|
||||
|
||||
|
||||
# Split the artifact URL to get information
|
||||
# Ex: https://github.com/element-hq/element-x-android/actions/runs/9065756777
|
||||
runUrl = args.runUrl
|
||||
|
||||
url_regex = r"https://github.com/(.+?)/(.+?)/actions/runs/(.+)"
|
||||
result = re.search(url_regex, runUrl)
|
||||
|
||||
if result is None:
|
||||
print(
|
||||
"❌ Invalid parameter --runUrl '%s'. Please check the format.\nIt should be something like: %s" %
|
||||
(runUrl, 'https://github.com/element-hq/element-x-android/actions/runs/9065756777')
|
||||
)
|
||||
exit(1)
|
||||
|
||||
(gitHubRepoOwner, gitHubRepo, runId) = result.groups()
|
||||
|
||||
if args.verbose:
|
||||
print("gitHubRepoOwner: %s, gitHubRepo: %s, runId: %s" % (gitHubRepoOwner, gitHubRepo, runId))
|
||||
|
||||
headers = {
|
||||
'Authorization': "Bearer %s" % args.token,
|
||||
'Accept': 'application/vnd.github+json'
|
||||
}
|
||||
|
||||
base_url = "https://api.github.com/repos/%s/%s/actions/runs/%s" % (gitHubRepoOwner, gitHubRepo, runId)
|
||||
|
||||
### Fetch build state
|
||||
status = ""
|
||||
data = "{}"
|
||||
while status != "completed":
|
||||
r = requests.get(base_url, headers=headers)
|
||||
data = json.loads(r.content.decode())
|
||||
|
||||
if args.verbose:
|
||||
print("Json data:")
|
||||
print(data)
|
||||
status = data.get("status")
|
||||
|
||||
if data.get("status") == "completed":
|
||||
if data.get("conclusion") != "success":
|
||||
print("❌ The action %s is completed, but there is an error, the conclusion is: %s." % (runUrl, data.get("conclusion")))
|
||||
exit(1)
|
||||
else:
|
||||
# Wait 1 minute
|
||||
print("The action %s is not completed yet, waiting 1 minute..." % runUrl)
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
artifacts_url = data.get("artifacts_url")
|
||||
if args.verbose:
|
||||
print("Artifacts url: %s" % artifacts_url)
|
||||
|
||||
r = requests.get(artifacts_url, headers=headers)
|
||||
data = json.loads(r.content.decode())
|
||||
|
||||
if args.directory == "":
|
||||
targetDir = runId
|
||||
else:
|
||||
targetDir = args.directory
|
||||
|
||||
for artifact in data.get("artifacts"):
|
||||
if args.verbose:
|
||||
print("Artifact:")
|
||||
print(artifact)
|
||||
# Invoke the script to download the artifact
|
||||
os.system("python3 ./tools/github/download_github_artifacts.py -t %s -d %s -a %s" % (args.token, targetDir, artifact.get("url")))
|
||||
159
tools/github/download_github_artifacts.py
Executable file
159
tools/github/download_github_artifacts.py
Executable file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2022-2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
#
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
# Run `pip3 install requests` if not installed yet
|
||||
import requests
|
||||
# Run `pip3 install re` if not installed yet
|
||||
import re
|
||||
|
||||
# This script downloads artifacts from GitHub.
|
||||
# Ref: https://docs.github.com/en/rest/actions/artifacts#get-an-artifact
|
||||
|
||||
error = False
|
||||
|
||||
### Arguments
|
||||
|
||||
parser = argparse.ArgumentParser(description='Download artifacts from GitHub.')
|
||||
parser.add_argument('-t',
|
||||
'--token',
|
||||
required=True,
|
||||
help='The GitHub token with read access.')
|
||||
parser.add_argument('-a',
|
||||
'--artifactUrl',
|
||||
required=True,
|
||||
help='the artifact_url from GitHub.')
|
||||
parser.add_argument('-f',
|
||||
'--filename',
|
||||
help='the filename, if not provided, will use the artifact name.')
|
||||
parser.add_argument('-i',
|
||||
'--ignoreErrors',
|
||||
help='Ignore errors that can be ignored. Build state and number of artifacts.',
|
||||
action="store_true")
|
||||
parser.add_argument('-d',
|
||||
'--directory',
|
||||
default="",
|
||||
help='the target directory, where files will be downloaded. If not provided the artifactId will be used to create a directory.')
|
||||
parser.add_argument('-v',
|
||||
'--verbose',
|
||||
help="increase output verbosity.",
|
||||
action="store_true")
|
||||
parser.add_argument('-s',
|
||||
'--simulate',
|
||||
help="simulate action, do not create folder or download any file.",
|
||||
action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
print("Argument:")
|
||||
print(args)
|
||||
|
||||
|
||||
# Split the artifact URL to get information
|
||||
# Ex: https://github.com/element-hq/element-x-android/actions/runs/7299827320/artifacts/1131077517
|
||||
artifactUrl = args.artifactUrl
|
||||
|
||||
# if artifactUrl starts with https://github.com
|
||||
if artifactUrl.startswith('https://github.com'):
|
||||
url_regex = r"https://github.com/(.+?)/(.+?)/actions/runs/.+?/artifacts/(.+)"
|
||||
result = re.search(url_regex, artifactUrl)
|
||||
|
||||
if result is None:
|
||||
print(
|
||||
"❌ Invalid parameter --artifactUrl '%s'. Please check the format.\nIt should be something like: %s" %
|
||||
(artifactUrl, 'https://github.com/element-hq/element-x-android/actions/runs/7299827320/artifacts/1131077517')
|
||||
)
|
||||
exit(1)
|
||||
else:
|
||||
url_regex = r"https://api.github.com/repos/(.+?)/(.+?)/actions/artifacts/(.+)"
|
||||
result = re.search(url_regex, artifactUrl)
|
||||
if result is None:
|
||||
print(
|
||||
"❌ Invalid parameter --artifactUrl '%s'. Please check the format.\nIt should be something like: %s" %
|
||||
(artifactUrl, 'https://api.github.com/repos/element-hq/element-x-android/actions/artifacts/1131077517')
|
||||
)
|
||||
exit(1)
|
||||
|
||||
|
||||
(gitHubRepoOwner, gitHubRepo, artifactId) = result.groups()
|
||||
|
||||
if args.verbose:
|
||||
print("gitHubRepoOwner: %s, gitHubRepo: %s, artifactId: %s" % (gitHubRepoOwner, gitHubRepo, artifactId))
|
||||
|
||||
headers = {
|
||||
'Authorization': "Bearer %s" % args.token,
|
||||
'Accept': 'application/vnd.github+json'
|
||||
}
|
||||
base_url = "https://api.github.com/repos/%s/%s/actions/artifacts/%s" % (gitHubRepoOwner, gitHubRepo, artifactId)
|
||||
|
||||
### Fetch build state
|
||||
|
||||
print("Getting artifacts data of project '%s/%s' artifactId '%s'..." % (gitHubRepoOwner, gitHubRepo, artifactId))
|
||||
|
||||
if args.verbose:
|
||||
print("Url: %s" % base_url)
|
||||
|
||||
r = requests.get(base_url, headers=headers)
|
||||
data = json.loads(r.content.decode())
|
||||
|
||||
if args.verbose:
|
||||
print("Json data:")
|
||||
print(data)
|
||||
|
||||
if args.verbose:
|
||||
print("Create subfolder %s to download artifacts..." % artifactId)
|
||||
|
||||
if args.directory == "":
|
||||
targetDir = artifactId
|
||||
else:
|
||||
targetDir = args.directory
|
||||
|
||||
if not args.simulate:
|
||||
os.makedirs(targetDir, exist_ok=True)
|
||||
|
||||
url = data.get("archive_download_url")
|
||||
if args.filename is not None:
|
||||
filename = args.filename
|
||||
else:
|
||||
filename = data.get("name") + ".zip"
|
||||
|
||||
## Print some info about the artifact origin
|
||||
commitLink = "https://github.com/%s/%s/commit/%s" % (gitHubRepoOwner, gitHubRepo, data.get("workflow_run").get("head_sha"))
|
||||
print("Preparing to download artifact `%s`, built from branch: `%s` (commit %s)" % (data.get("name"), data.get("workflow_run").get("head_branch"), commitLink))
|
||||
|
||||
if args.verbose:
|
||||
print()
|
||||
print("Artifact url: %s" % url)
|
||||
|
||||
target = targetDir + "/" + filename
|
||||
sizeInBytes = data.get("size_in_bytes")
|
||||
print("Downloading %s to '%s' (file size is %s bytes, this may take a while)..." % (filename, targetDir, sizeInBytes))
|
||||
if not args.simulate:
|
||||
# open file to write in binary mode
|
||||
with open(target, "wb") as file:
|
||||
# get request
|
||||
response = requests.get(url, headers=headers)
|
||||
# write to file
|
||||
file.write(response.content)
|
||||
print("Verifying file size...")
|
||||
# get the file size
|
||||
size = os.path.getsize(target)
|
||||
if sizeInBytes != size:
|
||||
# error = True
|
||||
print("Warning, file size mismatch: expecting %s and get %s. This is just a warning for now..." % (sizeInBytes, size))
|
||||
|
||||
if error:
|
||||
print("❌ Error(s) occurred, please check the log")
|
||||
exit(1)
|
||||
else:
|
||||
print("Done!")
|
||||
126
tools/lint/lint.xml
Normal file
126
tools/lint/lint.xml
Normal file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!--
|
||||
~ Copyright (c) 2025 Element Creations Ltd.
|
||||
~ Copyright 2022 New Vector Ltd.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
~ Please see LICENSE files in the repository root for full details.
|
||||
-->
|
||||
<lint>
|
||||
|
||||
<!-- Ensure this file does not contain unknown Ids -->
|
||||
<issue id="UnknownIssueId" severity="warning" />
|
||||
|
||||
<!-- Modify some severity -->
|
||||
|
||||
<!-- Resource -->
|
||||
<issue id="MissingTranslation" severity="ignore" />
|
||||
<issue id="TypographyEllipsis" severity="error" />
|
||||
<issue id="ImpliedQuantity" severity="error" />
|
||||
<issue id="MissingQuantity" severity="warning" />
|
||||
<issue id="UnusedQuantity" severity="error" />
|
||||
<issue id="IconXmlAndPng" severity="error" />
|
||||
<issue id="IconDipSize" severity="error" />
|
||||
<issue id="IconDuplicatesConfig" severity="error" />
|
||||
<issue id="IconDuplicates" severity="error" />
|
||||
<issue id="IconExpectedSize" severity="error">
|
||||
<ignore path="**/ic_launcher_monochrome.webp" />
|
||||
</issue>
|
||||
<issue id="LocaleFolder" severity="error" />
|
||||
|
||||
<!-- AlwaysShowAction is considered as an error to force ignoring the issue when detected -->
|
||||
<issue id="AlwaysShowAction" severity="error" />
|
||||
|
||||
<issue id="TooManyViews" severity="warning">
|
||||
<!-- Ignore TooManyViews in debug build type -->
|
||||
<ignore path="**/src/debug/**" />
|
||||
</issue>
|
||||
|
||||
<issue id="UnusedResources" severity="error">
|
||||
<!-- Ignore unused strings resource from localazy -->
|
||||
<ignore path="**/localazy.xml" />
|
||||
<!-- Ignore unused resource in debug build type -->
|
||||
<ignore path="**/src/debug/**" />
|
||||
<!-- Ignore unused resources in designsystem since they're imported elsewhere through aliases and can't be properly detected -->
|
||||
<ignore path="**/libraries/designsystem/src/main/res/**" />
|
||||
</issue>
|
||||
|
||||
<!-- UX -->
|
||||
<issue id="ButtonOrder" severity="error" />
|
||||
<issue id="TextFields" severity="error" />
|
||||
|
||||
<!-- Accessibility -->
|
||||
<issue id="LabelFor" severity="error" />
|
||||
<issue id="ContentDescription" severity="error" />
|
||||
<issue id="SpUsage" severity="error" />
|
||||
|
||||
<!-- Layout -->
|
||||
<issue id="UnknownIdInLayout" severity="error" />
|
||||
<issue id="StringFormatCount" severity="error" />
|
||||
<issue id="HardcodedText" severity="error" />
|
||||
<issue id="ObsoleteLayoutParam" severity="error" />
|
||||
<issue id="InefficientWeight" severity="error" />
|
||||
<issue id="DisableBaselineAlignment" severity="error" />
|
||||
<issue id="ScrollViewSize" severity="error" />
|
||||
<issue id="NegativeMargin" severity="error" />
|
||||
|
||||
<!-- RTL -->
|
||||
<issue id="RtlEnabled" severity="error" />
|
||||
<issue id="RtlHardcoded" severity="error" />
|
||||
<issue id="RtlSymmetry" severity="error" />
|
||||
|
||||
<!-- Code -->
|
||||
<issue id="NewApi" severity="error" />
|
||||
<issue id="SetTextI18n" severity="error" />
|
||||
<issue id="ViewConstructor" severity="error" />
|
||||
<issue id="UseValueOf" severity="error" />
|
||||
<issue id="ObsoleteSdkInt" severity="error" />
|
||||
<issue id="Recycle" severity="error" />
|
||||
<issue id="KotlinPropertyAccess" severity="error" />
|
||||
<issue id="DefaultLocale" severity="error" />
|
||||
<issue id="CheckResult" severity="error" />
|
||||
<issue id="StaticFieldLeak" severity="error" />
|
||||
|
||||
<issue id="InvalidPackage">
|
||||
<!-- Ignore error from HtmlCompressor lib -->
|
||||
<ignore path="**/htmlcompressor-1.4.jar" />
|
||||
<!-- Ignore error from dropbox-core-sdk-3.0.8 lib, which comes with Jitsi library -->
|
||||
<ignore path="**/dropbox-core-sdk-3.0.8.jar" />
|
||||
</issue>
|
||||
|
||||
<!-- Manifest -->
|
||||
<issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" />
|
||||
<issue id="DataExtractionRules" severity="error" />
|
||||
|
||||
<!-- Performance -->
|
||||
<issue id="UselessParent" severity="error" />
|
||||
|
||||
<!-- Dependencies -->
|
||||
<issue id="KtxExtensionAvailable" severity="error" />
|
||||
|
||||
<!-- Timber -->
|
||||
<!-- This rule is failing on CI because it's marked as unknwown rule id :/-->
|
||||
<!-- <issue id="BinaryOperationInTimber" severity="error" />-->
|
||||
<issue id="LogNotTimber" severity="error" />
|
||||
|
||||
<!-- Wording -->
|
||||
<issue id="Typos" severity="error" />
|
||||
<issue id="TypographyDashes" severity="error" />
|
||||
<issue id="PluralsCandidate" severity="error" />
|
||||
|
||||
<!-- Notification -->
|
||||
<issue id="LaunchActivityFromNotification" severity="error" />
|
||||
|
||||
<!-- We handle them manually -->
|
||||
<issue id="EnsureInitializerMetadata" severity="ignore" />
|
||||
|
||||
<!-- DI -->
|
||||
<!-- issue id="JvmStaticProvidesInObjectDetector" severity="error" /-->
|
||||
|
||||
<!-- Compose -->
|
||||
<issue id="UnnecessaryComposedModifier" severity="error" />
|
||||
|
||||
<!-- There seems to be an issue with this check, it flags lots of false positives. -->
|
||||
<!-- See https://issuetracker.google.com/issues/349411310 -->
|
||||
<!-- TODO: check again in the near future. -->
|
||||
<issue id="ProduceStateDoesNotAssignValue" severity="ignore" />
|
||||
</lint>
|
||||
76
tools/localazy/README.md
Normal file
76
tools/localazy/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Localazy
|
||||
|
||||
Localazy is used to host the source strings and their translations.
|
||||
|
||||
<!--- TOC -->
|
||||
|
||||
* [Localazy project](#localazy-project)
|
||||
* [Key naming rules](#key-naming-rules)
|
||||
* [Special suffixes](#special-suffixes)
|
||||
* [Placeholders](#placeholders)
|
||||
* [CLI Installation](#cli-installation)
|
||||
* [Download translations](#download-translations)
|
||||
* [Add translations to a specific module](#add-translations-to-a-specific-module)
|
||||
|
||||
<!--- END -->
|
||||
|
||||
## Localazy project
|
||||
|
||||
[](https://localazy.com/p/element)
|
||||
|
||||
To add new strings, or to translate existing strings, go the the Localazy project: [https://localazy.com/p/element](https://localazy.com/p/element). Please follow the key naming rules (see below).
|
||||
|
||||
Never edit manually the files `localazy.xml` or `translations.xml`!.
|
||||
|
||||
### Key naming rules
|
||||
|
||||
For code clarity and in order to download strings to the correct module, here are some naming rules to follow as much as possible:
|
||||
|
||||
- Keys for common strings, i.e. strings that can be used at multiple places must start by `action.` if this is a verb, or `common.` if not;
|
||||
- Keys for common accessibility strings must start by `a11y.`. Example: `a11y.hide_password`;
|
||||
- Keys for common strings should be named to match the string. Example: `action.copy_link` for the string `Copy link`;
|
||||
- When creating common strings, make sure to enable "Use dot (.) to create nested keys";
|
||||
- Keys for strings used in a single screen must start with `screen_` followed by the screen name, followed by a free name. Example: `screen_onboarding_welcome_title`;
|
||||
- Keys can have `_title` or `_subtitle` suffixes. Example: `screen_onboarding_welcome_title`, `screen_change_server_subtitle`;
|
||||
- For dialogs, keys can have `_dialog_title`, `_dialog_content`, and `_dialog_submit` suffixes. Example: `screen_signout_confirmation_dialog_title`, `screen_signout_confirmation_dialog_content`, `screen_signout_confirmation_dialog_submit`;
|
||||
- `a11y.` pattern can be used for strings that are only used for accessibility. Example: `a11y.hide_password`, `screen_roomlist_a11y_create_message`;
|
||||
- Strings for error message can start by `error_`, or contain `_error_` if used in a specific screen only. Example: `error_some_messages_have_not_been_sent`, `screen_change_server_error_invalid_homeserver`;
|
||||
|
||||
*Note*: those rules applies for `strings` and for `plurals`.
|
||||
|
||||
#### Special suffixes
|
||||
|
||||
- if a key is suffixed by `_ios`, it will not be imported in the Android project;
|
||||
- if a key is suffixed by `_android`, it will not be imported in the iOS project.
|
||||
|
||||
So feel free to use those suffixes when necessary for instance when the string content is referring to something related to Android only, or iOS only.
|
||||
|
||||
#### Placeholders
|
||||
|
||||
Placeholders should have the form `%1$s`, `%1$d`, etc.. Please use numbered placeholders. Note that Localazy will take care of converting the placeholder to Android (-> `%1$s`) and iOS specific format (-> `%1$@`). Ideally add a comment on Localazy to explain with what the placeholder(s) will be replaced at runtime.
|
||||
|
||||
## CLI Installation
|
||||
|
||||
To install the Localazy client, follow the instructions from [here](https://localazy.com/docs/cli/installation).
|
||||
|
||||
## Download translations
|
||||
|
||||
In the root folder of the project, run:
|
||||
|
||||
```shell
|
||||
./tools/localazy/downloadStrings.sh
|
||||
```
|
||||
|
||||
It will update all the `localazy.xml` resource files. In case of merge conflicts, just erase the files and download again using the script.
|
||||
|
||||
To also include the translations, i.e. the `translations.xml` files, add `--all` argument:
|
||||
|
||||
```shell
|
||||
./tools/localazy/downloadStrings.sh --all
|
||||
```
|
||||
|
||||
## Add translations to a specific module
|
||||
|
||||
Edit the file [config.json](./config.json) to add a new module, or add a new item in `includeRegex` arrays, then run the script again to see the effect.
|
||||
|
||||
[generateLocalazyConfig.py](generateLocalazyConfig.py) is the Python script that convert `config.json` to a localazy configuration file. Generally you should not edit this file.
|
||||
79
tools/localazy/checkForbiddenTerms.py
Executable file
79
tools/localazy/checkForbiddenTerms.py
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import re
|
||||
import sys
|
||||
from xml.dom import minidom
|
||||
|
||||
file = sys.argv[1]
|
||||
|
||||
# Dict of forbidden terms, with exceptions for some String name
|
||||
# Keys are the terms, values are the exceptions.
|
||||
forbiddenTerms = {
|
||||
r"\bElement\b": [
|
||||
# Those 2 strings are only used in debug version
|
||||
"screen_advanced_settings_element_call_base_url",
|
||||
"screen_advanced_settings_element_call_base_url_description",
|
||||
# only used for element.io homeserver, so it's fine
|
||||
"screen_server_confirmation_message_login_element_dot_io",
|
||||
# "Be in your element", will probably be changed on the forks, so we can ignore.
|
||||
"screen_onboarding_welcome_title",
|
||||
# Contains "Element Call"
|
||||
"screen_incoming_call_subtitle_android",
|
||||
"call_invalid_audio_device_bluetooth_devices_disabled",
|
||||
# Contains "Element X"
|
||||
"screen_room_timeline_legacy_call",
|
||||
# We explicitly want to mention Element Pro in these 2:
|
||||
"screen_change_server_error_element_pro_required_title",
|
||||
"screen_change_server_error_element_pro_required_message",
|
||||
]
|
||||
}
|
||||
|
||||
content = minidom.parse(file)
|
||||
|
||||
errors = []
|
||||
|
||||
### Strings
|
||||
for elem in content.getElementsByTagName('string'):
|
||||
name = elem.attributes['name'].value
|
||||
# Continue if value is empty
|
||||
child = elem.firstChild
|
||||
if child is None:
|
||||
# Should not happen
|
||||
continue
|
||||
value = child.nodeValue
|
||||
# If value contains a forbidden term, add the error to errors
|
||||
for (term, exceptions) in forbiddenTerms.items():
|
||||
matches = re.search(term, value)
|
||||
if matches and name not in exceptions:
|
||||
errors.append('Forbidden term "' + term + '" in string: "' + name + '": ' + value)
|
||||
|
||||
### Plurals
|
||||
for elem in content.getElementsByTagName('plurals'):
|
||||
name = elem.attributes['name'].value
|
||||
for it in elem.childNodes:
|
||||
if it.nodeType != it.ELEMENT_NODE:
|
||||
continue
|
||||
# Continue if value is empty
|
||||
child = it.firstChild
|
||||
if child is None:
|
||||
# Should not happen
|
||||
continue
|
||||
value = child.nodeValue
|
||||
# If value contains a forbidden term, add the error to errors
|
||||
for (term, exceptions) in forbiddenTerms.items():
|
||||
matches = re.search(term, value)
|
||||
if matches and name not in exceptions:
|
||||
errors.append('Forbidden term "' + term + '" in plural: "' + name + '": ' + value)
|
||||
|
||||
# If errors is not empty print the report
|
||||
if errors:
|
||||
print('Error(s) in file ' + file + ":", file=sys.stderr)
|
||||
for error in errors:
|
||||
print(" - " + error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
389
tools/localazy/config.json
Normal file
389
tools/localazy/config.json
Normal file
@@ -0,0 +1,389 @@
|
||||
{
|
||||
"modules" : [
|
||||
{
|
||||
"name" : ":appnav",
|
||||
"includeRegex" : [
|
||||
"banner\\.migrate_to_native_sliding_sync\\.force_logout.title",
|
||||
"banner\\.migrate_to_native_sliding_sync\\.action",
|
||||
"banner\\.migrate_to_native_sliding_sync\\.app_force_logout\\.title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:rageshake:impl",
|
||||
"includeRegex" : [
|
||||
"screen_bug_report_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:rageshake:api",
|
||||
"includeRegex" : [
|
||||
"crash_detection_.*",
|
||||
"rageshake_detection_.*",
|
||||
"settings_rageshake.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:announcement:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.space_announcement\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:logout:impl",
|
||||
"includeRegex" : [
|
||||
"screen_signout_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:deactivation:impl",
|
||||
"includeRegex" : [
|
||||
"screen_deactivate_account_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roomaliasresolver:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_alias_resolver_.*",
|
||||
"screen.join_room.loading_alert_title"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:signedout:impl",
|
||||
"includeRegex" : [
|
||||
"screen_signed_out_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:invite:impl",
|
||||
"includeRegex" : [
|
||||
"screen_invites_.*",
|
||||
"screen\\.join_room\\.decline_and_block_.*",
|
||||
"screen\\.decline_and_block\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:createroom:impl",
|
||||
"includeRegex" : [
|
||||
"screen_create_room_.*",
|
||||
"screen\\.create_room\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:startchat:impl",
|
||||
"includeRegex" : [
|
||||
"screen_start_chat_.*",
|
||||
"screen\\.start_chat\\..*",
|
||||
"screen_room_directory_search_title",
|
||||
"screen_create_room_action_create_room"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:verifysession:impl",
|
||||
"includeRegex" : [
|
||||
"screen_session_verification_.*",
|
||||
"screen_signout_in_progress_dialog_content",
|
||||
"screen_identity_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:textcomposer:impl",
|
||||
"includeRegex" : [
|
||||
"rich_text_editor.*",
|
||||
".*voice_message_tooltip",
|
||||
"screen\\.media_upload_preview.caption_warning"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:dateformatter:impl",
|
||||
"includeRegex" : [
|
||||
"common\\.date\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:permissions:api",
|
||||
"includeRegex" : [
|
||||
"dialog\\.permission_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:androidutils",
|
||||
"includeRegex" : [
|
||||
"error_no_compatible_app_found"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:mediaviewer:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.media_details\\..*",
|
||||
"screen_media_browser_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:eventformatter:impl",
|
||||
"includeRegex" : [
|
||||
"state_event_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:push:impl",
|
||||
"includeRegex" : [
|
||||
"push_.*",
|
||||
"notification_.*",
|
||||
"notification\\..*",
|
||||
"troubleshoot_notifications\\.test_blocked_users\\..*",
|
||||
"troubleshoot_notifications_test_current_push_provider.*",
|
||||
"troubleshoot_notifications_test_detect_push_provider.*",
|
||||
"troubleshoot_notifications_test_display_notification_.*",
|
||||
"troubleshoot_notifications_test_push_loop_back_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:permissions:impl",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_test_check_permission_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:pushproviders:firebase",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_test_firebase_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:pushproviders:unifiedpush",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_test_unified_push_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:login:impl",
|
||||
"includeRegex" : [
|
||||
"screen_onboarding_.*",
|
||||
"screen_login_.*",
|
||||
"screen_server_confirmation_.*",
|
||||
"screen_change_server_.*",
|
||||
"screen_change_account_provider_.*",
|
||||
"screen_create_account_.*",
|
||||
"screen_account_provider_.*",
|
||||
"screen_waitlist_.*",
|
||||
"screen_qr_code_login_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:leaveroom:api",
|
||||
"includeRegex" : [
|
||||
"leave_room_alert_.*",
|
||||
"leave_conversation_alert_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:home:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.home\\..*",
|
||||
"screen_roomlist_.*",
|
||||
"screen\\.roomlist\\..*",
|
||||
"session_verification_banner_.*",
|
||||
"confirm_recovery_key_banner_.*",
|
||||
"banner\\.set_up_recovery\\..*",
|
||||
"banner\\.battery_optimization\\..*",
|
||||
"banner\\.new_sound\\..*",
|
||||
"full_screen_intent_banner_.*",
|
||||
"screen_migration_.*",
|
||||
"screen_invites_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roomdetails:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_details_.*",
|
||||
"screen\\.room_details\\..*",
|
||||
"screen_room_member_list_.*",
|
||||
"screen\\.room_member_list\\..*",
|
||||
"screen_room_notification_settings_.*",
|
||||
"screen_notification_settings_edit_failed_updating_default_mode",
|
||||
"screen_polls_history_title",
|
||||
"screen_notification_settings_mentions_only_disclaimer",
|
||||
"screen_room_change_.*",
|
||||
"screen_room_roles_.*",
|
||||
"screen\\.edit_room_address\\..*",
|
||||
"screen\\.security_and_privacy\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:space:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.leave_space\\..*",
|
||||
"screen\\.space_settings\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:userprofile:shared",
|
||||
"includeRegex" : [
|
||||
"screen_start_chat_error_starting_chat",
|
||||
"screen_dm_details_.*",
|
||||
"screen_room_member_details_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:invitepeople:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.invite_users\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:messages:impl",
|
||||
"includeRegex" : [
|
||||
"emoji_picker_category_.*",
|
||||
"screen_report_content_.*",
|
||||
"screen_room_attachment.*",
|
||||
"screen_room_encrypted.*",
|
||||
"screen_room_invite.*",
|
||||
"screen\\.room\\.mention.*",
|
||||
"screen_room_message.*",
|
||||
"screen_room_retry.*",
|
||||
"screen_room_timeline.*",
|
||||
"screen\\.room_timeline.*",
|
||||
"screen_room_typing.*",
|
||||
"screen\\.media_upload.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:analytics:impl",
|
||||
"includeRegex" : [
|
||||
"screen_analytics_prompt.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:analytics:api",
|
||||
"includeRegex" : [
|
||||
"screen_analytics_settings_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:ftue:impl",
|
||||
"includeRegex" : [
|
||||
"screen_welcome_.*",
|
||||
"screen_notification_optin_.*",
|
||||
"screen_identity_.*",
|
||||
"screen_session_verification_enter_recovery_key"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:poll:impl",
|
||||
"includeRegex" : [
|
||||
"screen_create_poll_.*",
|
||||
"screen_edit_poll_.*",
|
||||
"screen_polls_history_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:poll:api",
|
||||
"includeRegex" : [
|
||||
"a11y\\.polls\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:securebackup:impl",
|
||||
"includeRegex" : [
|
||||
"screen_chat_backup_.*",
|
||||
"screen_key_backup_disable_.*",
|
||||
"screen_recovery_key_.*",
|
||||
"screen_create_new_recovery_key_.*",
|
||||
"screen_encryption_reset.*",
|
||||
"screen_reset_encryption.*",
|
||||
"screen\\.reset_encryption.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:preferences:impl",
|
||||
"includeRegex" : [
|
||||
"screen_advanced_settings_.*",
|
||||
"screen\\.advanced_settings\\..*",
|
||||
"screen_edit_profile_.*",
|
||||
"screen_notification_settings_.*",
|
||||
"screen_blocked_users_.*",
|
||||
"full_screen_intent_banner_.*",
|
||||
"troubleshoot_notifications_entry_point_.*",
|
||||
"screen\\.labs\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:troubleshoot:impl",
|
||||
"includeRegex" : [
|
||||
"troubleshoot_notifications_screen_.*",
|
||||
"screen\\.push_history\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:matrixui",
|
||||
"includeRegex" : [
|
||||
"screen_invites_invited_you",
|
||||
"screen\\.bottom_sheet\\.create_dm\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:call:impl",
|
||||
"includeRegex" : [
|
||||
"call_.*",
|
||||
"screen_incoming_call.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:lockscreen:impl",
|
||||
"includeRegex" : [
|
||||
"screen_app_lock_.*",
|
||||
"screen_signout_in_progress_dialog_content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roomdirectory:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_directory_.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:joinroom:impl",
|
||||
"includeRegex" : [
|
||||
"screen_join_room_.*",
|
||||
"screen\\.join_room\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:knockrequests:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.knock_requests_list\\..*",
|
||||
"screen\\.room\\.single_knock_request.*",
|
||||
"screen\\.room\\.multiple_knock_requests.*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:reportroom:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.report_room\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:roommembermoderation:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.bottom_sheet\\.manage_room_member\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:rolesandpermissions:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_change_.*",
|
||||
"screen_room_roles_.*",
|
||||
"screen\\.room_roles_and_permissions\\..*",
|
||||
"screen_room_member_list.*",
|
||||
"screen\\.room_member_list\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:securityandprivacy:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.edit_room_address\\..*",
|
||||
"screen\\.security_and_privacy\\..*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
48
tools/localazy/downloadStrings.sh
Executable file
48
tools/localazy/downloadStrings.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#! /bin/bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
set -e
|
||||
|
||||
if [[ $1 == "--all" ]]; then
|
||||
echo "Note: I will update all the files."
|
||||
allFiles=1
|
||||
else
|
||||
echo "Note: I will update only the English files."
|
||||
allFiles=0
|
||||
fi
|
||||
|
||||
echo "Generating the configuration file for localazy..."
|
||||
python3 ./tools/localazy/generateLocalazyConfig.py $allFiles
|
||||
|
||||
echo "Deleting all existing localazy.xml files..."
|
||||
find . -name 'localazy.xml' -delete
|
||||
|
||||
if [[ $allFiles == 1 ]]; then
|
||||
echo "Deleting all existing translations.xml files..."
|
||||
find . -name 'translations.xml' -delete
|
||||
fi
|
||||
|
||||
echo "Importing the strings..."
|
||||
localazy download --config ./tools/localazy/localazy.json
|
||||
|
||||
echo "Removing the generated config"
|
||||
rm ./tools/localazy/localazy.json
|
||||
|
||||
echo "Formatting the resources files..."
|
||||
find . -name 'localazy.xml' -exec ./tools/localazy/formatXmlResourcesFile.py {} \;
|
||||
if [[ $allFiles == 1 ]]; then
|
||||
find . -name 'translations.xml' -exec ./tools/localazy/formatXmlResourcesFile.py {} \;
|
||||
fi
|
||||
|
||||
echo "Checking forbidden terms..."
|
||||
find . -name 'localazy.xml' -exec ./tools/localazy/checkForbiddenTerms.py {} \;
|
||||
if [[ $allFiles == 1 ]]; then
|
||||
find . -name 'translations.xml' -exec ./tools/localazy/checkForbiddenTerms.py {} \;
|
||||
fi
|
||||
|
||||
echo "Success!"
|
||||
83
tools/localazy/formatXmlResourcesFile.py
Executable file
83
tools/localazy/formatXmlResourcesFile.py
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import re
|
||||
import sys
|
||||
from xml.dom import minidom
|
||||
|
||||
file = sys.argv[1]
|
||||
|
||||
content = minidom.parse(file)
|
||||
|
||||
# sort content by value of tag name
|
||||
newContent = minidom.Document()
|
||||
resources = newContent.createElement('resources')
|
||||
resources.setAttribute('xmlns:xliff', 'urn:oasis:names:tc:xliff:document:1.2')
|
||||
newContent.appendChild(resources)
|
||||
|
||||
resource = dict()
|
||||
|
||||
### Strings
|
||||
for elem in content.getElementsByTagName('string'):
|
||||
name = elem.attributes['name'].value
|
||||
# Continue if value is empty
|
||||
child = elem.firstChild
|
||||
if child is None:
|
||||
# Print an error to stderr
|
||||
print('Warning: Empty content for string: ' + name + " in file " + file, file=sys.stderr)
|
||||
continue
|
||||
value = child.nodeValue
|
||||
# Continue if string is empty
|
||||
if value == '""':
|
||||
# Print an error to stderr
|
||||
print('Warning: Empty string value for string: ' + name + " in file " + file, file=sys.stderr)
|
||||
continue
|
||||
resource[name] = elem.cloneNode(True)
|
||||
|
||||
### Plurals
|
||||
for elem in content.getElementsByTagName('plurals'):
|
||||
plural = newContent.createElement('plurals')
|
||||
name = elem.attributes['name'].value
|
||||
plural.setAttribute('name', name)
|
||||
for it in elem.childNodes:
|
||||
if it.nodeType != it.ELEMENT_NODE:
|
||||
continue
|
||||
# Continue if value is empty
|
||||
child = it.firstChild
|
||||
if child is None:
|
||||
# Print an error to stderr
|
||||
print('Warning: Empty content for plurals: ' + name + " in file " + file, file=sys.stderr)
|
||||
continue
|
||||
value = child.nodeValue
|
||||
# Continue if string is empty
|
||||
if value == '""':
|
||||
# Print an error to stderr
|
||||
print('Warning: Empty item value for plurals: ' + name + " in file " + file, file=sys.stderr)
|
||||
continue
|
||||
plural.appendChild(it.cloneNode(True))
|
||||
if plural.hasChildNodes():
|
||||
resource[name] = plural
|
||||
|
||||
for key in sorted(resource.keys()):
|
||||
resources.appendChild(resource[key])
|
||||
|
||||
result = newContent.toprettyxml(indent=" ") \
|
||||
.replace('<?xml version="1.0" ?>', '<?xml version="1.0" encoding="utf-8"?>') \
|
||||
.replace('"', '"') \
|
||||
.replace('...', '…')
|
||||
|
||||
## Replace space by unbreakable space before punctuation
|
||||
result = re.sub(r" ([\?\!\:…])", r" \1", result)
|
||||
|
||||
# Special treatment for French wording
|
||||
if 'values-fr' in file:
|
||||
## Replace ' with ’
|
||||
result = re.sub(r"([cdjlmnsu])\\\'", r"\1’", result, flags=re.IGNORECASE)
|
||||
|
||||
with open(file, "w") as text_file:
|
||||
text_file.write(result)
|
||||
112
tools/localazy/generateLocalazyConfig.py
Executable file
112
tools/localazy/generateLocalazyConfig.py
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
# Read the config.json file
|
||||
with open('./tools/localazy/config.json', 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
allFiles = sys.argv[1] == "1"
|
||||
|
||||
# Convert a module name to a path
|
||||
# Ex: ":features:verifysession:impl" => "features/verifysession/impl"
|
||||
def convertModuleToPath(name):
|
||||
return name[1:].replace(":", "/")
|
||||
|
||||
# Regex that will be excluded from the Android project, you may add items here if necessary.
|
||||
regexToAlwaysExclude = [
|
||||
"Notification",
|
||||
".*_ios"
|
||||
]
|
||||
|
||||
baseAction = {
|
||||
"type": "android",
|
||||
# Replacement done in all string values
|
||||
"replacements": {
|
||||
"...": "…"
|
||||
},
|
||||
"params": {
|
||||
"force_underscore": "yes"
|
||||
}
|
||||
}
|
||||
|
||||
# Store all regex specific to module, to exclude the corresponding key from the common string module
|
||||
allRegexToExcludeFromMainModule = []
|
||||
# All actions that will be serialized in the localazy config
|
||||
allActions = []
|
||||
|
||||
# Iterating on the config
|
||||
for entry in config["modules"]:
|
||||
# Create action for the default language
|
||||
excludeRegex = regexToAlwaysExclude
|
||||
if "excludeRegex" in entry:
|
||||
excludeRegex += entry["excludeRegex"]
|
||||
action = baseAction | {
|
||||
"output": convertModuleToPath(entry["name"]) + "/src/main/res/values/localazy.xml",
|
||||
"includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])),
|
||||
"excludeKeys": list(map(lambda i: "REGEX:" + i, excludeRegex)),
|
||||
"conditions": [
|
||||
"equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json"
|
||||
]
|
||||
}
|
||||
# print(action)
|
||||
allActions.append(action)
|
||||
# Create action for the translations
|
||||
if allFiles:
|
||||
actionTranslation = baseAction | {
|
||||
"output": convertModuleToPath(entry["name"]) + "/src/main/res/values-${langAndroidResNoScript}/translations.xml",
|
||||
"includeKeys": list(map(lambda i: "REGEX:" + i, entry["includeRegex"])),
|
||||
"excludeKeys": list(map(lambda i: "REGEX:" + i, excludeRegex)),
|
||||
"conditions": [
|
||||
"!equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json"
|
||||
],
|
||||
"langAliases": {
|
||||
"id": "in"
|
||||
}
|
||||
}
|
||||
allActions.append(actionTranslation)
|
||||
allRegexToExcludeFromMainModule.extend(entry["includeRegex"])
|
||||
|
||||
# Append configuration for the main string module: default language
|
||||
mainAction = baseAction | {
|
||||
"output": "libraries/ui-strings/src/main/res/values/localazy.xml",
|
||||
"excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)),
|
||||
"conditions": [
|
||||
"equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json"
|
||||
]
|
||||
}
|
||||
# print(mainAction)
|
||||
allActions.append(mainAction)
|
||||
|
||||
if allFiles:
|
||||
# Append configuration for the main string module: translations
|
||||
mainActionTranslation = baseAction | {
|
||||
"output": "libraries/ui-strings/src/main/res/values-${langAndroidResNoScript}/translations.xml",
|
||||
"excludeKeys": list(map(lambda i: "REGEX:" + i, allRegexToExcludeFromMainModule + regexToAlwaysExclude)),
|
||||
"conditions": [
|
||||
"!equals: ${langAndroidResNoScript}, en | equals: ${file}, content.json"
|
||||
],
|
||||
"langAliases": {
|
||||
"id": "in"
|
||||
}
|
||||
}
|
||||
allActions.append(mainActionTranslation)
|
||||
|
||||
# Generate the configuration for localazy
|
||||
result = {
|
||||
"readKey": "a7876306080832595063-aa37154bb3772f6146890fca868d155b2228b492c56c91f67abdcdfb74d6142d",
|
||||
"conversion": {
|
||||
"actions": allActions
|
||||
}
|
||||
}
|
||||
|
||||
# Json serialization
|
||||
with open('./tools/localazy/localazy.json', 'w') as json_file:
|
||||
json.dump(result, json_file, indent=4, sort_keys=True)
|
||||
85
tools/localazy/importSupportedLocalesFromLocalazy.py
Executable file
85
tools/localazy/importSupportedLocalesFromLocalazy.py
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
|
||||
def getLocalesFromLocalazy():
|
||||
command = subprocess.run(
|
||||
["localazy languages --read-key a7876306080832595063-aa37154bb3772f6146890fca868d155b2228b492c56c91f67abdcdfb74d6142d --csv"],
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
data = command.stdout
|
||||
result = []
|
||||
for line in data.split("\n"):
|
||||
if line:
|
||||
line = line.split(",")
|
||||
if (line[6] == "true"):
|
||||
result.append(line[0])
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def normalizeForResourceConfigurations(locale):
|
||||
match locale:
|
||||
case "id":
|
||||
return "in"
|
||||
case "zh_TW#Hant":
|
||||
return "zh-rTW"
|
||||
case "pt_BR":
|
||||
return "pt-rBR"
|
||||
case "zh#Hans":
|
||||
return "zh-rCN"
|
||||
case "en_US":
|
||||
return "en-rUS"
|
||||
case _:
|
||||
return locale
|
||||
|
||||
|
||||
def normalizeForLocalConfig(locale):
|
||||
match locale:
|
||||
case "id":
|
||||
return "in"
|
||||
case "zh_TW#Hant":
|
||||
return "zh-TW"
|
||||
case "zh#Hans":
|
||||
return "zh-CN"
|
||||
case _:
|
||||
return locale
|
||||
|
||||
|
||||
def generateLocaleFile(locales, file):
|
||||
with open("plugins/src/main/kotlin/extension/locales.kt", "w") as f:
|
||||
f.write("// File generated by " + file + ", do not edit\n\n")
|
||||
f.write("package extension\n\n")
|
||||
f.write("val locales = setOf(\n")
|
||||
for locale in locales:
|
||||
f.write(" \"" + normalizeForResourceConfigurations(locale) + "\",\n")
|
||||
f.write(")\n")
|
||||
|
||||
|
||||
def generateLocalesConfigFile(locales, file):
|
||||
with open("app/src/main/res/xml/locales_config.xml", "w") as f:
|
||||
f.write("<!-- File generated by " + file + ", do not edit -->\n")
|
||||
f.write('<locale-config xmlns:android="http://schemas.android.com/apk/res/android">\n')
|
||||
for locale in locales:
|
||||
f.write(" <locale android:name=\"" + normalizeForLocalConfig(locale) + "\"/>\n")
|
||||
f.write("</locale-config>\n")
|
||||
|
||||
|
||||
def main():
|
||||
file = os.path.basename(__file__)
|
||||
locales = getLocalesFromLocalazy()
|
||||
generateLocaleFile(locales, file)
|
||||
generateLocalesConfigFile(locales, file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
tools/quality/check.sh
Executable file
22
tools/quality/check.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# List of tasks to run before creating a PR, to limit the risk of getting rejected by the CI.
|
||||
# Can be used as a git hook if you want.
|
||||
|
||||
# exit when any command fails
|
||||
set -e
|
||||
|
||||
# First run the quickest script
|
||||
./tools/check/check_code_quality.sh
|
||||
|
||||
# Check ktlint and Konsist first
|
||||
./gradlew runQualityChecks
|
||||
|
||||
# Build, test and check the project, with warning as errors
|
||||
./gradlew check -PallWarningsAsErrors=true
|
||||
3
tools/release/ReleaseNotesNightly.md
Normal file
3
tools/release/ReleaseNotesNightly.md
Normal file
@@ -0,0 +1,3 @@
|
||||
See which PRs have been merged recently here:
|
||||
|
||||
https://github.com/element-hq/element-x-android/pulls?q=is%3Apr+sort%3Aupdated-desc+is%3Aclosed
|
||||
227
tools/release/fix-pg-map-id.py
Normal file
227
tools/release/fix-pg-map-id.py
Normal file
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/python3
|
||||
# encoding: utf-8
|
||||
# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman <flx@obfusk.net>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later.
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import zipfile
|
||||
import zlib
|
||||
|
||||
from binascii import hexlify
|
||||
from typing import Any, Dict, Match, Tuple
|
||||
|
||||
DEX_MAGIC = b"dex\n"
|
||||
DEX_MAGIC_RE = re.compile(rb"dex\n(\d{3})\x00")
|
||||
|
||||
PROF_MAGIC = b"pro\x00"
|
||||
PROF_010_P = b"010\x00"
|
||||
|
||||
CLASSES_DEX_RE = re.compile(r"classes\d*\.dex")
|
||||
ASSET_PROF = "assets/dexopt/baseline.prof"
|
||||
|
||||
PG_MAP_ID_RE = re.compile(rb'(~~R8{"backend":"dex".*?"pg-map-id":")([0-9a-f]{7})(")')
|
||||
|
||||
ATTRS = ("compress_type", "create_system", "create_version", "date_time",
|
||||
"external_attr", "extract_version", "flag_bits")
|
||||
LEVELS = (9, 6, 4, 1)
|
||||
|
||||
|
||||
class Error(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
# FIXME: is there a better alternative?
|
||||
class ReproducibleZipInfo(zipfile.ZipInfo):
|
||||
"""Reproducible ZipInfo hack."""
|
||||
|
||||
if "_compresslevel" not in zipfile.ZipInfo.__slots__: # type: ignore[attr-defined]
|
||||
if "compress_level" not in zipfile.ZipInfo.__slots__: # type: ignore[attr-defined]
|
||||
raise Error("zipfile.ZipInfo has no ._compresslevel")
|
||||
|
||||
_compresslevel: int
|
||||
_override: Dict[str, Any] = {}
|
||||
|
||||
def __init__(self, zinfo: zipfile.ZipInfo, **override: Any) -> None:
|
||||
# pylint: disable=W0231
|
||||
if override:
|
||||
self._override = {**self._override, **override}
|
||||
for k in self.__slots__:
|
||||
if hasattr(zinfo, k):
|
||||
setattr(self, k, getattr(zinfo, k))
|
||||
|
||||
def __getattribute__(self, name: str) -> Any:
|
||||
if name != "_override":
|
||||
try:
|
||||
return self._override[name]
|
||||
except KeyError:
|
||||
pass
|
||||
return object.__getattribute__(self, name)
|
||||
|
||||
|
||||
def fix_pg_map_id(input_dir: str, output_dir: str, map_id: str) -> None:
|
||||
file_data = {}
|
||||
for filename in [ASSET_PROF] + sorted(os.listdir(input_dir)):
|
||||
if re.fullmatch(CLASSES_DEX_RE, filename) or filename == ASSET_PROF:
|
||||
print(f"reading {filename!r}...")
|
||||
with open(os.path.join(input_dir, *filename.split("/")), "rb") as fh:
|
||||
file_data[filename] = fh.read()
|
||||
_fix_pg_map_id(file_data, map_id)
|
||||
for filename, data in file_data.items():
|
||||
print(f"writing {filename!r}...")
|
||||
if "/" in filename:
|
||||
os.makedirs(os.path.join(output_dir, *filename.split("/")[:-1]), exist_ok=True)
|
||||
with open(os.path.join(output_dir, *filename.split("/")), "wb") as fh:
|
||||
fh.write(data)
|
||||
|
||||
|
||||
def fix_pg_map_id_apk(input_apk: str, output_apk: str, map_id: str) -> None:
|
||||
with open(input_apk, "rb") as fh_raw:
|
||||
with zipfile.ZipFile(input_apk) as zf_in:
|
||||
with zipfile.ZipFile(output_apk, "w") as zf_out:
|
||||
file_data = {}
|
||||
for info in zf_in.infolist():
|
||||
if re.fullmatch(CLASSES_DEX_RE, info.filename) or info.filename == ASSET_PROF:
|
||||
print(f"reading {info.filename!r}...")
|
||||
file_data[info.filename] = zf_in.read(info)
|
||||
_fix_pg_map_id(file_data, map_id)
|
||||
for info in zf_in.infolist():
|
||||
attrs = {attr: getattr(info, attr) for attr in ATTRS}
|
||||
zinfo = ReproducibleZipInfo(info, **attrs)
|
||||
if info.compress_type == 8:
|
||||
fh_raw.seek(info.header_offset)
|
||||
n, m = struct.unpack("<HH", fh_raw.read(30)[26:30])
|
||||
fh_raw.seek(info.header_offset + 30 + m + n)
|
||||
ccrc = 0
|
||||
size = info.compress_size
|
||||
while size > 0:
|
||||
ccrc = zlib.crc32(fh_raw.read(min(size, 4096)), ccrc)
|
||||
size -= 4096
|
||||
with zf_in.open(info) as fh_in:
|
||||
comps = {lvl: zlib.compressobj(lvl, 8, -15) for lvl in LEVELS}
|
||||
ccrcs = {lvl: 0 for lvl in LEVELS}
|
||||
while True:
|
||||
data = fh_in.read(4096)
|
||||
if not data:
|
||||
break
|
||||
for lvl in LEVELS:
|
||||
ccrcs[lvl] = zlib.crc32(comps[lvl].compress(data), ccrcs[lvl])
|
||||
for lvl in LEVELS:
|
||||
if ccrc == zlib.crc32(comps[lvl].flush(), ccrcs[lvl]):
|
||||
zinfo._compresslevel = lvl
|
||||
break
|
||||
else:
|
||||
raise Error(f"Unable to determine compresslevel for {info.filename!r}")
|
||||
elif info.compress_type != 0:
|
||||
raise Error(f"Unsupported compress_type {info.compress_type}")
|
||||
if re.fullmatch(CLASSES_DEX_RE, info.filename) or info.filename == ASSET_PROF:
|
||||
print(f"writing {info.filename!r}...")
|
||||
zf_out.writestr(zinfo, file_data[info.filename])
|
||||
else:
|
||||
with zf_in.open(info) as fh_in:
|
||||
with zf_out.open(zinfo, "w") as fh_out:
|
||||
while True:
|
||||
data = fh_in.read(4096)
|
||||
if not data:
|
||||
break
|
||||
fh_out.write(data)
|
||||
|
||||
|
||||
def _fix_pg_map_id(file_data: Dict[str, bytes], map_id: str) -> None:
|
||||
crcs = {}
|
||||
for filename in file_data:
|
||||
if re.fullmatch(CLASSES_DEX_RE, filename):
|
||||
print(f"fixing {filename!r}...")
|
||||
data = _fix_dex_id_checksum(file_data[filename], map_id.encode())
|
||||
file_data[filename] = data
|
||||
crcs[filename] = zlib.crc32(data)
|
||||
if ASSET_PROF in file_data:
|
||||
print(f"fixing {ASSET_PROF!r}...")
|
||||
file_data[ASSET_PROF] = _fix_prof_checksum(file_data[ASSET_PROF], crcs)
|
||||
|
||||
|
||||
def _fix_dex_id_checksum(data: bytes, map_id: bytes) -> bytes:
|
||||
def repl(m: Match[bytes]) -> bytes:
|
||||
print(f"fixing pg-map-id: {m.group(2)!r} -> {map_id!r}")
|
||||
return m.group(1) + map_id + m.group(3)
|
||||
|
||||
magic = data[:8]
|
||||
if magic[:4] != DEX_MAGIC or not DEX_MAGIC_RE.fullmatch(magic):
|
||||
raise Error(f"Unsupported magic {magic!r}")
|
||||
print(f"dex version={int(magic[4:7]):03d}")
|
||||
checksum, signature = struct.unpack("<I20s", data[8:32])
|
||||
fixed_data = re.sub(PG_MAP_ID_RE, repl, data[32:])
|
||||
if fixed_data == data[32:]:
|
||||
print("(not modified)")
|
||||
return data
|
||||
fixed_sig = hashlib.sha1(fixed_data).digest()
|
||||
print(f"fixing signature: {hexlify(signature).decode()} -> {hexlify(fixed_sig).decode()}")
|
||||
fixed_data = fixed_sig + fixed_data
|
||||
fixed_checksum = zlib.adler32(fixed_data)
|
||||
print(f"fixing checksum: 0x{checksum:x} -> 0x{fixed_checksum:x}")
|
||||
return magic + int.to_bytes(fixed_checksum, 4, "little") + fixed_data
|
||||
|
||||
|
||||
def _fix_prof_checksum(data: bytes, crcs: Dict[str, int]) -> bytes:
|
||||
magic, data = _split(data, 4)
|
||||
version, data = _split(data, 4)
|
||||
if magic == PROF_MAGIC:
|
||||
if version == PROF_010_P:
|
||||
print("prof version=010 P")
|
||||
return PROF_MAGIC + PROF_010_P + _fix_prof_010_p_checksum(data, crcs)
|
||||
else:
|
||||
raise Error(f"Unsupported prof version {version!r}")
|
||||
else:
|
||||
raise Error(f"Unsupported magic {magic!r}")
|
||||
|
||||
|
||||
def _fix_prof_010_p_checksum(data: bytes, crcs: Dict[str, int]) -> bytes:
|
||||
num_dex_files, uncompressed_data_size, compressed_data_size, data = _unpack("<BII", data)
|
||||
dex_data_headers = []
|
||||
if len(data) != compressed_data_size:
|
||||
raise Error("Compressed data size does not match")
|
||||
data = zlib.decompress(data)
|
||||
if len(data) != uncompressed_data_size:
|
||||
raise Error("Uncompressed data size does not match")
|
||||
for i in range(num_dex_files):
|
||||
profile_key_size, num_type_ids, hot_method_region_size, \
|
||||
dex_checksum, num_method_ids, data = _unpack("<HHIII", data)
|
||||
profile_key, data = _split(data, profile_key_size)
|
||||
filename = profile_key.decode()
|
||||
fixed_checksum = crcs[filename]
|
||||
if fixed_checksum != dex_checksum:
|
||||
print(f"fixing {filename!r} checksum: 0x{dex_checksum:x} -> 0x{fixed_checksum:x}")
|
||||
dex_data_headers.append(struct.pack(
|
||||
"<HHIII", profile_key_size, num_type_ids, hot_method_region_size,
|
||||
fixed_checksum, num_method_ids) + profile_key)
|
||||
fixed_data = b"".join(dex_data_headers) + data
|
||||
fixed_cdata = zlib.compress(fixed_data, 1)
|
||||
fixed_hdr = struct.pack("<BII", num_dex_files, uncompressed_data_size, len(fixed_cdata))
|
||||
return fixed_hdr + fixed_cdata
|
||||
|
||||
|
||||
def _unpack(fmt: str, data: bytes) -> Any:
|
||||
assert all(c in "<BHI" for c in fmt)
|
||||
size = fmt.count("B") + 2 * fmt.count("H") + 4 * fmt.count("I")
|
||||
return struct.unpack(fmt, data[:size]) + (data[size:],)
|
||||
|
||||
|
||||
def _split(data: bytes, size: int) -> Tuple[bytes, bytes]:
|
||||
return data[:size], data[size:]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(prog="fix-pg-map-id.py")
|
||||
parser.add_argument("input_dir_or_apk", metavar="INPUT_DIR_OR_APK")
|
||||
parser.add_argument("output_dir_or_apk", metavar="OUTPUT_DIR_OR_APK")
|
||||
parser.add_argument("pg_map_id", metavar="PG_MAP_ID")
|
||||
args = parser.parse_args()
|
||||
if os.path.isdir(args.input_dir_or_apk):
|
||||
fix_pg_map_id(args.input_dir_or_apk, args.output_dir_or_apk, args.pg_map_id)
|
||||
else:
|
||||
fix_pg_map_id_apk(args.input_dir_or_apk, args.output_dir_or_apk, args.pg_map_id)
|
||||
|
||||
# vim: set tw=80 sw=4 sts=4 et fdm=marker :
|
||||
199
tools/release/inplace-fix.py
Normal file
199
tools/release/inplace-fix.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/python3
|
||||
# encoding: utf-8
|
||||
# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman <flx@obfusk.net>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
COMMANDS = (
|
||||
"fix-compresslevel",
|
||||
"fix-files",
|
||||
"fix-newlines",
|
||||
"fix-pg-map-id",
|
||||
"rm-files",
|
||||
"sort-apk",
|
||||
"sort-baseline",
|
||||
)
|
||||
|
||||
BUILD_TOOLS_WITH_BROKEN_ZIPALIGN = ("31.0.0", "32.0.0")
|
||||
BUILD_TOOLS_WITH_PAGE_SIZE_FROM = "35.0.0-rc1"
|
||||
SDK_ENV = ("ANDROID_HOME", "ANDROID_SDK", "ANDROID_SDK_ROOT")
|
||||
|
||||
|
||||
def _zipalign_cmd(page_align: bool, page_size: Optional[int]) -> Tuple[str, ...]:
|
||||
if page_align:
|
||||
if page_size is not None:
|
||||
return ("zipalign", "-P", str(page_size), "4")
|
||||
return ("zipalign", "-p", "4")
|
||||
return ("zipalign", "4")
|
||||
|
||||
|
||||
ZIPALIGN = _zipalign_cmd(page_align=False, page_size=None)
|
||||
ZIPALIGN_P = _zipalign_cmd(page_align=True, page_size=None)
|
||||
|
||||
|
||||
class Error(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def inplace_fix(command: str, input_file: str, *args: str,
|
||||
zipalign: bool = False, page_align: bool = False,
|
||||
page_size: Optional[int] = None, internal: bool = False) -> None:
|
||||
if command not in COMMANDS:
|
||||
raise Error(f"Unknown command {command}")
|
||||
exe, script = _script_cmd(command)
|
||||
ext = os.path.splitext(input_file)[1]
|
||||
with tempfile.TemporaryDirectory() as tdir:
|
||||
fixed = os.path.join(tdir, "fixed" + ext)
|
||||
run_command(exe, script, input_file, fixed, *args, trim=2)
|
||||
if zipalign:
|
||||
aligned = os.path.join(tdir, "aligned" + ext)
|
||||
zac = zipalign_cmd(page_align=page_align, page_size=page_size, internal=internal)
|
||||
run_command(*zac, fixed, aligned, trim=2)
|
||||
print(f"[MOVE] {aligned} to {input_file}")
|
||||
shutil.move(aligned, input_file)
|
||||
else:
|
||||
print(f"[MOVE] {fixed} to {input_file}")
|
||||
shutil.move(fixed, input_file)
|
||||
|
||||
|
||||
def zipalign_cmd(page_align: bool = False, page_size: Optional[int] = None,
|
||||
internal: bool = False) -> Tuple[str, ...]:
|
||||
"""
|
||||
Find zipalign command using $PATH or $ANDROID_HOME etc.
|
||||
|
||||
>>> zipalign_cmd()
|
||||
('zipalign', '4')
|
||||
>>> zipalign_cmd(page_align=True)
|
||||
('zipalign', '-p', '4')
|
||||
>>> zipalign_cmd(page_align=True, page_size=16)
|
||||
('zipalign', '-P', '16', '4')
|
||||
>>> cmd = zipalign_cmd(page_align=True, page_size=16, internal=True)
|
||||
>>> [x.split("/")[-1] for x in cmd]
|
||||
['python3', 'zipalign.py', '-P', '16', '4']
|
||||
>>> os.environ["PATH"] = ""
|
||||
>>> for k in SDK_ENV:
|
||||
... os.environ[k] = ""
|
||||
>>> cmd = zipalign_cmd()
|
||||
>>> [x.split("/")[-1] for x in cmd]
|
||||
['python3', 'zipalign.py', '4']
|
||||
>>> os.environ["ANDROID_HOME"] = "test/fake-sdk"
|
||||
>>> zipalign_cmd()
|
||||
[SKIP BROKEN] 31.0.0
|
||||
[FOUND] test/fake-sdk/build-tools/30.0.3/zipalign
|
||||
('test/fake-sdk/build-tools/30.0.3/zipalign', '4')
|
||||
>>> cmd = zipalign_cmd(page_align=True, page_size=16)
|
||||
[SKIP TOO OLD] 31.0.0
|
||||
[SKIP TOO OLD] 30.0.3
|
||||
[SKIP TOO OLD] 26.0.2
|
||||
>>> [x.split("/")[-1] for x in cmd]
|
||||
['python3', 'zipalign.py', '-P', '16', '4']
|
||||
>>> os.environ["ANDROID_HOME"] = "test/fake-sdk-2"
|
||||
>>> zipalign_cmd(page_align=True, page_size=16)
|
||||
[FOUND] test/fake-sdk-2/build-tools/35.0.0-rc1/zipalign
|
||||
('test/fake-sdk-2/build-tools/35.0.0-rc1/zipalign', '-P', '16', '4')
|
||||
|
||||
"""
|
||||
cmd, *args = _zipalign_cmd(page_align, page_size)
|
||||
if not internal:
|
||||
if shutil.which(cmd):
|
||||
return (cmd, *args)
|
||||
for k in SDK_ENV:
|
||||
if home := os.environ.get(k):
|
||||
tools = os.path.join(home, "build-tools")
|
||||
if os.path.exists(tools):
|
||||
for vsn in sorted(os.listdir(tools), key=_vsn, reverse=True):
|
||||
if page_size and _vsn(vsn) < _vsn(BUILD_TOOLS_WITH_PAGE_SIZE_FROM):
|
||||
print(f"[SKIP TOO OLD] {vsn}")
|
||||
continue
|
||||
for s in BUILD_TOOLS_WITH_BROKEN_ZIPALIGN:
|
||||
if vsn.startswith(s):
|
||||
print(f"[SKIP BROKEN] {vsn}")
|
||||
break
|
||||
else:
|
||||
c = os.path.join(tools, vsn, cmd)
|
||||
if shutil.which(c):
|
||||
print(f"[FOUND] {c}")
|
||||
return (c, *args)
|
||||
return (*_script_cmd(cmd), *args)
|
||||
|
||||
|
||||
def _vsn(v: str) -> Tuple[int, ...]:
|
||||
"""
|
||||
>>> vs = "31.0.0 32.1.0-rc1 34.0.0-rc3 34.0.0 35.0.0-rc1".split()
|
||||
>>> for v in sorted(vs, key=_vsn, reverse=True):
|
||||
... (_vsn(v), v)
|
||||
((35, 0, 0, 0, 1), '35.0.0-rc1')
|
||||
((34, 0, 0, 1, 0), '34.0.0')
|
||||
((34, 0, 0, 0, 3), '34.0.0-rc3')
|
||||
((32, 1, 0, 0, 1), '32.1.0-rc1')
|
||||
((31, 0, 0, 1, 0), '31.0.0')
|
||||
"""
|
||||
if "-rc" in v:
|
||||
v = v.replace("-rc", ".0.", 1)
|
||||
else:
|
||||
v = v + ".1.0"
|
||||
return tuple(int(x) if x.isdigit() else -1 for x in v.split("."))
|
||||
|
||||
|
||||
def _script_cmd(command: str) -> Tuple[str, str]:
|
||||
script_dir = os.path.dirname(__file__)
|
||||
for cmd in (command, command.replace("-", "_")):
|
||||
script = os.path.join(script_dir, cmd + ".py")
|
||||
if os.path.exists(script):
|
||||
break
|
||||
else:
|
||||
raise Error(f"Script for {command} not found")
|
||||
exe = sys.executable or "python3"
|
||||
return exe, script
|
||||
|
||||
|
||||
def run_command(*args: str, trim: int = 1) -> None:
|
||||
targs = tuple(os.path.basename(a) for a in args[:trim]) + args[trim:]
|
||||
print(f"[RUN] {' '.join(targs)}")
|
||||
try:
|
||||
subprocess.run(args, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Error(f"{args[0]} command failed") from e
|
||||
except FileNotFoundError as e:
|
||||
raise Error(f"{args[0]} command not found") from e
|
||||
|
||||
|
||||
def main() -> None:
|
||||
prog = os.path.basename(sys.argv[0])
|
||||
usage = (f"{prog} [-h] [--zipalign] [--page-align] [--page-size N] [--internal]\n"
|
||||
f"{len('usage: ' + prog) * ' '} COMMAND INPUT_FILE [...]")
|
||||
epilog = f"Commands: {', '.join(COMMANDS)}."
|
||||
parser = argparse.ArgumentParser(usage=usage, epilog=epilog)
|
||||
parser.add_argument("--zipalign", action="store_true",
|
||||
help="run zipalign after COMMAND")
|
||||
parser.add_argument("--page-align", action="store_true",
|
||||
help="run zipalign w/ -p option (implies --zipalign)")
|
||||
parser.add_argument("--page-size", metavar="N", type=int,
|
||||
help="run zipalign w/ -P N option (implies --page-align)")
|
||||
parser.add_argument("--internal", action="store_true",
|
||||
help="use zipalign.py instead of searching $PATH/$ANDROID_HOME/etc.")
|
||||
parser.add_argument("command", metavar="COMMAND")
|
||||
parser.add_argument("input_file", metavar="INPUT_FILE")
|
||||
args, rest = parser.parse_known_args()
|
||||
try:
|
||||
inplace_fix(args.command, args.input_file, *rest,
|
||||
zipalign=bool(args.zipalign or args.page_align or args.page_size),
|
||||
page_align=bool(args.page_align or args.page_size),
|
||||
page_size=args.page_size, internal=args.internal)
|
||||
except Error as e:
|
||||
print(f"Error: {e}.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
# vim: set tw=80 sw=4 sts=4 et fdm=marker :
|
||||
415
tools/release/release.sh
Executable file
415
tools/release/release.sh
Executable file
@@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# do not exit when any command fails (issue with git flow)
|
||||
set +e
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "| Welcome to the release script! |\n"
|
||||
printf "================================================================================\n"
|
||||
|
||||
printf "Checking environment...\n"
|
||||
envError=0
|
||||
|
||||
# Check that bundletool is installed
|
||||
if ! command -v bundletool &> /dev/null
|
||||
then
|
||||
printf "Fatal: bundletool is not installed. You can install it running \`brew install bundletool\`\n"
|
||||
envError=1
|
||||
fi
|
||||
|
||||
# Path of the key store (it's a file)
|
||||
keyStorePath="${ELEMENT_X_KEYSTORE_PATH}"
|
||||
if [[ -z "${keyStorePath}" ]]; then
|
||||
printf "Fatal: ELEMENT_X_KEYSTORE_PATH is not defined in the environment.\n"
|
||||
envError=1
|
||||
fi
|
||||
# Keystore password
|
||||
keyStorePassword="${ELEMENT_X_KEYSTORE_PASSWORD}"
|
||||
if [[ -z "${keyStorePassword}" ]]; then
|
||||
printf "Fatal: ELEMENT_X_KEYSTORE_PASSWORD is not defined in the environment.\n"
|
||||
envError=1
|
||||
fi
|
||||
# Key password
|
||||
keyPassword="${ELEMENT_X_KEY_PASSWORD}"
|
||||
if [[ -z "${keyPassword}" ]]; then
|
||||
printf "Fatal: ELEMENT_X_KEY_PASSWORD is not defined in the environment.\n"
|
||||
envError=1
|
||||
fi
|
||||
# GitHub token
|
||||
gitHubToken="${ELEMENT_GITHUB_TOKEN}"
|
||||
if [[ -z "${gitHubToken}" ]]; then
|
||||
printf "Fatal: ELEMENT_GITHUB_TOKEN is not defined in the environment.\n"
|
||||
envError=1
|
||||
fi
|
||||
# Android home
|
||||
androidHome="${ANDROID_HOME}"
|
||||
if [[ -z "${androidHome}" ]]; then
|
||||
printf "Fatal: ANDROID_HOME is not defined in the environment.\n"
|
||||
envError=1
|
||||
fi
|
||||
# @elementbot:matrix.org matrix token / Not mandatory
|
||||
elementBotToken="${ELEMENT_BOT_MATRIX_TOKEN}"
|
||||
if [[ -z "${elementBotToken}" ]]; then
|
||||
printf "Warning: ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment.\n"
|
||||
fi
|
||||
|
||||
if [ ${envError} == 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read minSdkVersion from file plugins/src/main/kotlin/Versions.kt
|
||||
minSdkVersion=$(grep "MIN_SDK_FOSS =" ./plugins/src/main/kotlin/Versions.kt |cut -d '=' -f 2 |xargs)
|
||||
# Read buildToolsVersion from file plugins/src/main/kotlin/Versions.kt
|
||||
buildToolsVersion=$(grep "BUILD_TOOLS_VERSION =" ./plugins/src/main/kotlin/Versions.kt |cut -d '=' -f 2 |xargs)
|
||||
buildToolsPath="${androidHome}/build-tools/${buildToolsVersion}"
|
||||
|
||||
if [[ ! -d ${buildToolsPath} ]]; then
|
||||
printf "Fatal: %s folder not found, ensure that you have installed the SDK version %s.\n" "${buildToolsPath}" "${buildToolsVersion}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if git flow is enabled
|
||||
gitFlowDevelop=$(git config gitflow.branch.develop)
|
||||
if [[ ${gitFlowDevelop} != "" ]]
|
||||
then
|
||||
printf "Git flow is initialized\n"
|
||||
else
|
||||
printf "Git flow is not initialized. Initializing...\n"
|
||||
./tools/gitflow/gitflow-init.sh
|
||||
fi
|
||||
|
||||
printf "OK\n"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Ensuring main and develop branches are up to date...\n"
|
||||
|
||||
git checkout main
|
||||
git pull
|
||||
git checkout develop
|
||||
git pull
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
# Guessing version to propose a default version
|
||||
versionsFile="./plugins/src/main/kotlin/Versions.kt"
|
||||
# Get current year on 2 digits
|
||||
versionYearCandidate=$(date +%y)
|
||||
currentVersionMonth=$(grep "val versionMonth" ${versionsFile} | cut -d " " -f6)
|
||||
# Get current month on 2 digits
|
||||
versionMonthCandidate=$(date +%m)
|
||||
versionMonthCandidateNoLeadingZero=${versionMonthCandidate/#0/}
|
||||
currentVersionReleaseNumber=$(grep "val versionReleaseNumber" ${versionsFile} | cut -d " " -f6)
|
||||
# if the current month is the same as the current version, we increment the release number, else we reset it to 0
|
||||
if [[ ${currentVersionMonth} -eq ${versionMonthCandidateNoLeadingZero} ]]; then
|
||||
versionReleaseNumberCandidate=$((currentVersionReleaseNumber + 1))
|
||||
else
|
||||
versionReleaseNumberCandidate=0
|
||||
fi
|
||||
versionCandidate="${versionYearCandidate}.${versionMonthCandidate}.${versionReleaseNumberCandidate}"
|
||||
|
||||
read -r -p "Please enter the release version (example: ${versionCandidate}). Format must be 'YY.MM.x' or 'YY.MM.xy'. Just press enter if ${versionCandidate} is correct. " version
|
||||
version=${version:-${versionCandidate}}
|
||||
|
||||
# extract year, month and release number for future use
|
||||
versionYear=$(echo "${version}" | cut -d "." -f1)
|
||||
versionMonth=$(echo "${version}" | cut -d "." -f2)
|
||||
versionMonthNoLeadingZero=${versionMonth/#0/}
|
||||
versionReleaseNumber=$(echo "${version}" | cut -d "." -f3)
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Starting the release %s\n" "${version}"
|
||||
git flow release start "${version}"
|
||||
|
||||
# Note: in case the release is already started and the script is started again, checkout the release branch again.
|
||||
ret=$?
|
||||
if [[ $ret -ne 0 ]]; then
|
||||
printf "Mmh, it seems that the release is already started. Checking out the release branch...\n"
|
||||
git checkout "release/${version}"
|
||||
fi
|
||||
|
||||
# Ensure version is OK
|
||||
versionsFileBak="${versionsFile}.bak"
|
||||
cp ${versionsFile} ${versionsFileBak}
|
||||
sed "s/private const val versionYear = .*/private const val versionYear = ${versionYear}/" ${versionsFileBak} > ${versionsFile}
|
||||
sed "s/private const val versionMonth = .*/private const val versionMonth = ${versionMonthNoLeadingZero}/" ${versionsFile} > ${versionsFileBak}
|
||||
sed "s/private const val versionReleaseNumber = .*/private const val versionReleaseNumber = ${versionReleaseNumber}/" ${versionsFileBak} > ${versionsFile}
|
||||
rm ${versionsFileBak}
|
||||
|
||||
git commit -a -m "Setting version for the release ${version}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Creating fastlane file...\n"
|
||||
printf -v versionReleaseNumber2Digits "%02d" "${versionReleaseNumber}"
|
||||
fastlaneFile="20${versionYear}${versionMonth}${versionReleaseNumber2Digits}0.txt"
|
||||
fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}"
|
||||
printf "Main changes in this version: bug fixes and improvements.\nFull changelog: https://github.com/element-hq/element-x-android/releases" > "${fastlanePathFile}"
|
||||
|
||||
read -r -p "I have created the file ${fastlanePathFile}, please edit it and press enter to continue. "
|
||||
git add "${fastlanePathFile}"
|
||||
git commit -a -m "Adding fastlane file for version ${version}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "OK, finishing the release...\n"
|
||||
git flow release finish "${version}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
read -r -p "Done, push the branch 'main' and the new tag (yes/no) default to yes? " doPush
|
||||
doPush=${doPush:-yes}
|
||||
|
||||
if [ "${doPush}" == "yes" ]; then
|
||||
printf "Pushing branch 'main' and tag 'v%s'...\n" "${version}"
|
||||
git push origin main
|
||||
git push origin "v${version}"
|
||||
else
|
||||
printf "Not pushing, do not forget to push manually!\n"
|
||||
fi
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Checking out develop...\n"
|
||||
git checkout develop
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "The GitHub action https://github.com/element-hq/element-x-android/actions/workflows/release.yml?query=branch%%3Amain should have start a new run.\n"
|
||||
read -r -p "Please enter the url of the run, no need to wait for it to complete (example: https://github.com/element-hq/element-x-android/actions/runs/9065756777): " runUrl
|
||||
|
||||
targetPath="./tmp/Element/${version}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Downloading the artifacts...\n"
|
||||
|
||||
ret=1
|
||||
|
||||
while [[ $ret -ne 0 ]]; do
|
||||
python3 ./tools/github/download_all_github_artifacts.py \
|
||||
--token "${gitHubToken}" \
|
||||
--runUrl "${runUrl}" \
|
||||
--directory "${targetPath}"
|
||||
|
||||
ret=$?
|
||||
if [[ $ret -ne 0 ]]; then
|
||||
read -r -p "Error while downloading the artifacts. You may want to fix the issue and retry. Retry (yes/no) default to yes? " doRetry
|
||||
doRetry=${doRetry:-yes}
|
||||
if [ "${doRetry}" == "no" ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Unzipping the F-Droid artifact...\n"
|
||||
|
||||
fdroidTargetPath="${targetPath}/fdroid"
|
||||
unzip "${targetPath}"/elementx-app-fdroid-apks-unsigned.zip -d "${fdroidTargetPath}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Patching the FDroid APKs using inplace-fix.py...\n"
|
||||
|
||||
inplaceFixScript="./tools/release/inplace-fix.py"
|
||||
python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-arm64-v8a-release.apk '0000000'
|
||||
python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release.apk '0000000'
|
||||
python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-x86-release.apk '0000000'
|
||||
python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-x86_64-release.apk '0000000'
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Signing the FDroid APKs...\n"
|
||||
|
||||
cp "${fdroidTargetPath}"/app-fdroid-arm64-v8a-release.apk \
|
||||
"${fdroidTargetPath}"/app-fdroid-arm64-v8a-release-signed.apk
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--alignment-preserved true \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
--key-pass pass:"${keyPassword}" \
|
||||
--min-sdk-version "${minSdkVersion}" \
|
||||
"${fdroidTargetPath}"/app-fdroid-arm64-v8a-release-signed.apk
|
||||
|
||||
cp "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release.apk \
|
||||
"${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release-signed.apk
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--alignment-preserved true \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
--key-pass pass:"${keyPassword}" \
|
||||
--min-sdk-version "${minSdkVersion}" \
|
||||
"${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release-signed.apk
|
||||
|
||||
cp "${fdroidTargetPath}"/app-fdroid-x86-release.apk \
|
||||
"${fdroidTargetPath}"/app-fdroid-x86-release-signed.apk
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--alignment-preserved true \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
--key-pass pass:"${keyPassword}" \
|
||||
--min-sdk-version "${minSdkVersion}" \
|
||||
"${fdroidTargetPath}"/app-fdroid-x86-release-signed.apk
|
||||
|
||||
cp "${fdroidTargetPath}"/app-fdroid-x86_64-release.apk \
|
||||
"${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--alignment-preserved true \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
--key-pass pass:"${keyPassword}" \
|
||||
--min-sdk-version "${minSdkVersion}" \
|
||||
"${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Please check the information below:\n"
|
||||
|
||||
printf "File app-fdroid-arm64-v8a-release-signed.apk:\n"
|
||||
"${buildToolsPath}"/aapt dump badging "${fdroidTargetPath}"/app-fdroid-arm64-v8a-release-signed.apk | grep package
|
||||
printf "File app-fdroid-armeabi-v7a-release-signed.apk:\n"
|
||||
"${buildToolsPath}"/aapt dump badging "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release-signed.apk | grep package
|
||||
printf "File app-fdroid-x86-release-signed.apk:\n"
|
||||
"${buildToolsPath}"/aapt dump badging "${fdroidTargetPath}"/app-fdroid-x86-release-signed.apk | grep package
|
||||
printf "File app-fdroid-x86_64-release-signed.apk:\n"
|
||||
"${buildToolsPath}"/aapt dump badging "${fdroidTargetPath}"/app-fdroid-x86_64-release-signed.apk | grep package
|
||||
|
||||
printf "\n"
|
||||
read -r -p "Does it look correct? Press enter when it's done. "
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "The APKs in %s have been signed!\n" "${fdroidTargetPath}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Unzipping the Gplay artifact...\n"
|
||||
|
||||
gplayTargetPath="${targetPath}/gplay"
|
||||
unzip "${targetPath}"/elementx-app-gplay-bundle-unsigned.zip -d "${gplayTargetPath}"
|
||||
|
||||
unsignedBundlePath="${gplayTargetPath}/app-gplay-release.aab"
|
||||
signedBundlePath="${gplayTargetPath}/app-gplay-release-signed.aab"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Signing file %s with build-tools version %s for min SDK version %s...\n" "${unsignedBundlePath}" "${buildToolsVersion}" "${minSdkVersion}"
|
||||
|
||||
cp "${unsignedBundlePath}" "${signedBundlePath}"
|
||||
|
||||
"${buildToolsPath}"/apksigner sign \
|
||||
-v \
|
||||
--ks "${keyStorePath}" \
|
||||
--ks-pass pass:"${keyStorePassword}" \
|
||||
--ks-key-alias elementx \
|
||||
--key-pass pass:"${keyPassword}" \
|
||||
--min-sdk-version "${minSdkVersion}" \
|
||||
"${signedBundlePath}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Please check the information below:\n"
|
||||
|
||||
printf "Version code: "
|
||||
bundletool dump manifest --bundle="${signedBundlePath}" --xpath=/manifest/@android:versionCode
|
||||
printf "Version name: "
|
||||
bundletool dump manifest --bundle="${signedBundlePath}" --xpath=/manifest/@android:versionName
|
||||
|
||||
printf "\n"
|
||||
read -r -p "Does it look correct? Press enter to continue. "
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "The file %s has been signed and can be uploaded to the PlayStore!\n" "${signedBundlePath}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
read -r -p "Do you want to build the APKs from the app bundle? You need to do this step if you want to install the application to your device. (yes/no) default to no " doBuildApks
|
||||
doBuildApks=${doBuildApks:-no}
|
||||
|
||||
if [ "${doBuildApks}" == "yes" ]; then
|
||||
printf "Building apks...\n"
|
||||
bundletool build-apks --bundle="${signedBundlePath}" --output="${gplayTargetPath}"/elementx.apks \
|
||||
--ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \
|
||||
--overwrite
|
||||
|
||||
read -r -p "Do you want to install the application to your device? Make sure there is one (and only one!) connected device first. (yes/no) default to yes " doDeploy
|
||||
doDeploy=${doDeploy:-yes}
|
||||
if [ "${doDeploy}" == "yes" ]; then
|
||||
printf "Installing apk for your device...\n"
|
||||
bundletool install-apks --apks="${gplayTargetPath}"/elementx.apks
|
||||
read -r -p "Please run the application on your phone to check that the upgrade went well. Press enter to continue. "
|
||||
else
|
||||
printf "APK will not be deployed!\n"
|
||||
fi
|
||||
else
|
||||
printf "APKs will not be generated!\n"
|
||||
fi
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Create the open testing release on GooglePlay.\n"
|
||||
|
||||
printf "On GooglePlay console, go the the open testing section and click on \"Create new release\" button, then:\n"
|
||||
printf " - upload the file %s.\n" "${signedBundlePath}"
|
||||
printf " - copy the release note from the fastlane file.\n"
|
||||
printf " - download the universal APK, to be able to provide it to the GitHub release: click on the right arrow next to the \"App bundle\", then click on the \"Download\" tab, and download the \"Signed, universal APK\".\n"
|
||||
printf " - submit the release.\n"
|
||||
read -r -p "Press enter to continue. "
|
||||
|
||||
printf "You can then go to \"Publishing overview\" and send the new release for a review by Google.\n"
|
||||
read -r -p "Press enter to continue. "
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
githubCreateReleaseLink="https://github.com/element-hq/element-x-android/releases/new?tag=v${version}&title=Element%20X%20Android%20v${version}"
|
||||
printf "Creating the release on gitHub.\n"
|
||||
printf -- "Open this link: %s\n" "${githubCreateReleaseLink}"
|
||||
printf "Then\n"
|
||||
printf " - Click on the 'Generate releases notes' button.\n"
|
||||
printf " - Optionally reorder items and fix typos.\n"
|
||||
printf " - Add the file %s to the GitHub release.\n" "${signedBundlePath}"
|
||||
printf " - Add the universal APK, downloaded from the GooglePlay console to the GitHub release.\n"
|
||||
printf " - Add the 4 signed APKs for F-Droid, located at %s to the GitHub release.\n" "${fdroidTargetPath}"
|
||||
read -r -p ". Press enter to continue. "
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Update the project release notes:\n\n"
|
||||
|
||||
read -r -p "Copy the content of the release note generated by GitHub to the file CHANGES.md and press enter to commit the change. "
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Committing...\n"
|
||||
git commit -a -m "Changelog for version ${version}"
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
read -r -p "Done, push the branch 'develop' (yes/no) default to yes? (A rebase may be necessary in case develop got new commits) " doPush
|
||||
doPush=${doPush:-yes}
|
||||
|
||||
if [ "${doPush}" == "yes" ]; then
|
||||
printf "Pushing branch 'develop'...\n"
|
||||
git push origin develop
|
||||
else
|
||||
printf "Not pushing, do not forget to push manually!\n"
|
||||
fi
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Message for the Android internal room:\n\n"
|
||||
message="@room Element X Android ${version} is ready to be tested. You can get it from https://github.com/element-hq/element-x-android/releases/tag/v${version}. You can install the universal APK. If you want to install the application from the app bundle, you can follow instructions [here](https://github.com/element-hq/element-x-android/blob/develop/docs/install_from_github_release.md). Please report any feedback. Thanks!"
|
||||
printf "%s\n\n" "${message}"
|
||||
|
||||
if [[ -z "${elementBotToken}" ]]; then
|
||||
read -r -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter to continue. "
|
||||
else
|
||||
read -r -p "Send this message to the room (yes/no) default to yes? " doSend
|
||||
doSend=${doSend:-yes}
|
||||
if [ "${doSend}" == "yes" ]; then
|
||||
printf "Sending message...\n"
|
||||
transactionId=$(openssl rand -hex 16)
|
||||
# Element Android internal
|
||||
matrixRoomId="!LiSLXinTDCsepePiYW:matrix.org"
|
||||
curl -X PUT --data "{\"msgtype\":\"m.text\",\"body\":\"${message}\"}" -H "Authorization: Bearer ${elementBotToken}" https://matrix-client.matrix.org/_matrix/client/r0/rooms/${matrixRoomId}/send/m.room.message/\$local."${transactionId}"
|
||||
else
|
||||
printf "Message not sent, please send it manually!\n"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\n================================================================================\n"
|
||||
printf "Congratulation! Kudos for using this script! Have a nice day!\n"
|
||||
printf "================================================================================\n"
|
||||
82
tools/rte/build_rte.sh
Executable file
82
tools/rte/build_rte.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# Ask to build from local source or to clone the repository
|
||||
read -p "Do you want to build the RTE from local source (yes/no) default to yes? " buildLocal
|
||||
buildLocal=${buildLocal:-yes}
|
||||
|
||||
date=$(gdate +%Y%m%d%H%M%S)
|
||||
elementPwd=$(pwd)
|
||||
|
||||
# Ask for the RTE local source path
|
||||
# if folder rte/ exists, use it as default
|
||||
if [ "${buildLocal}" == "yes" ]; then
|
||||
read -p "Please enter the path to the Rust SDK local source, default to ../matrix-rich-text-editor: " rtePath
|
||||
rtePath=${rtePath:-../matrix-rich-text-editor/}
|
||||
if [ ! -d "${rtePath}" ]; then
|
||||
printf "\nFolder ${rtePath} does not exist. Please clone the matrix-rich-text-editor repository in the folder ../matrix-rich-text-editor.\n\n"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
read -p "Please enter the RTE repository url, default to https://github.com/matrix-org/matrix-rich-text-editor.git " rteUrl
|
||||
rteUrl=${rteUrl:-https://github.com/matrix-org/matrix-rich-text-editor.git}
|
||||
read -p "Please enter the Rust SDK branch, default to main " rteBranch
|
||||
rteBranch=${rteBranch:-main}
|
||||
cd ..
|
||||
git clone "${rteUrl}" matrix-rich-text-editor-"$date"
|
||||
cd matrix-rich-text-editor-"$date"
|
||||
git checkout "${rteBranch}"
|
||||
rtePath=$(pwd)
|
||||
cd "${elementPwd}"
|
||||
fi
|
||||
|
||||
|
||||
cd "${rtePath}"
|
||||
git status
|
||||
|
||||
read -p "Will build with this version of the RTE ^. Is it correct (yes/no) default to yes? " rteCorrect
|
||||
rteCorrect=${rteCorrect:-yes}
|
||||
|
||||
if [ "${rteCorrect}" != "yes" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ask if the user wants to build the app after
|
||||
read -p "Do you want to build the app after (yes/no) default to yes? " buildApp
|
||||
buildApp=${buildApp:-yes}
|
||||
|
||||
cd "${elementPwd}"
|
||||
|
||||
cd "$rtePath"
|
||||
|
||||
printf "\nBuilding the RTE for aarch64...\n\n"
|
||||
make android-bindings-aarch64
|
||||
cd platforms/android
|
||||
./gradlew clean :library:assembleRelease :library-compose:assembleRelease
|
||||
cp ./library/build/outputs/aar/library-release.aar "$elementPwd"/libraries/textcomposer/lib/library.aar
|
||||
cp ./library-compose/build/outputs/aar/library-compose-release.aar "$elementPwd"/libraries/textcomposer/lib/library-compose.aar
|
||||
|
||||
cd "${elementPwd}"
|
||||
mkdir -p ./libraries/textcomposer/lib/versions
|
||||
cp ./libraries/textcomposer/lib/library.aar ./libraries/textcomposer/lib/versions/library-"${date}".aar
|
||||
cp ./libraries/textcomposer/lib/library-compose.aar ./libraries/textcomposer/lib/versions/library-compose-"${date}".aar
|
||||
|
||||
|
||||
if [ "${buildApp}" == "yes" ]; then
|
||||
printf "\nBuilding the application...\n\n"
|
||||
./gradlew assembleDebug
|
||||
fi
|
||||
|
||||
if [ "${buildLocal}" == "no" ]; then
|
||||
printf "\nCleaning up...\n\n"
|
||||
rm -rf ../matrix-rich-text-editor-"$date"
|
||||
fi
|
||||
|
||||
printf "\nDone!\n"
|
||||
75
tools/sas/import_sas_emojis.py
Executable file
75
tools/sas/import_sas_emojis.py
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2020-2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
# Run `pip3 install requests` if not installed yet
|
||||
import requests
|
||||
|
||||
### Arguments
|
||||
|
||||
parser = argparse.ArgumentParser(description='Download sas string from matrix-doc.')
|
||||
parser.add_argument('-v',
|
||||
'--verbose',
|
||||
help="increase output verbosity.",
|
||||
action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
print("Argument:")
|
||||
print(args)
|
||||
|
||||
base_url = "https://raw.githubusercontent.com/matrix-org/matrix-spec/main/data-definitions/sas-emoji.json"
|
||||
|
||||
base_emoji_url = "https://raw.githubusercontent.com/twitter/twemoji/master/assets/svg/"
|
||||
|
||||
print("Downloading " + base_url + "…")
|
||||
|
||||
r0 = requests.get(base_url)
|
||||
data0 = json.loads(r0.content.decode())
|
||||
|
||||
if args.verbose:
|
||||
print("Json data:")
|
||||
print(data0)
|
||||
|
||||
print()
|
||||
|
||||
scripts_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
data_defs_dir = os.path.join(scripts_dir, "../../tmp/emoji/")
|
||||
|
||||
def handle_emoji(dict):
|
||||
print("Handle emoji " + str(dict["number"]) + " (" + dict["description"] + ")…")
|
||||
if args.verbose:
|
||||
print("With")
|
||||
print(dict)
|
||||
# Transform dict["unicode"] from "U+2601U+FE0F" to "2601U"
|
||||
emoji = dict["unicode"].split("U+")[1].lower()
|
||||
url = base_emoji_url + emoji + ".svg"
|
||||
file = os.path.join(data_defs_dir, "ic_verification_" + format(dict["number"], '02d') + ".svg")
|
||||
print("Downloading " + url + " to " + file + "…")
|
||||
r = requests.get(url)
|
||||
if r.status_code != 200:
|
||||
print("Fatal: " + str(r.status_code))
|
||||
# Stop script with error
|
||||
sys.exit(1)
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
with open(file, "w") as f:
|
||||
f.write(r.content.decode())
|
||||
|
||||
for emoji in data0:
|
||||
handle_emoji(emoji)
|
||||
|
||||
print()
|
||||
print("Success!")
|
||||
print()
|
||||
print("To convert to vector drawable, download tool from https://www.androiddesignpatterns.com/2018/11/android-studio-svg-to-vector-cli.html")
|
||||
print("unzip it, and run:")
|
||||
print("vd-tool/bin/vd-tool -c -in ./tmp/emoji -out features/verifysession/impl/src/main/res/drawable")
|
||||
105
tools/sas/import_sas_strings.py
Executable file
105
tools/sas/import_sas_strings.py
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2020-2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
# Run `pip3 install requests` if not installed yet
|
||||
import requests
|
||||
|
||||
### Arguments
|
||||
|
||||
parser = argparse.ArgumentParser(description='Download sas string from matrix-doc.')
|
||||
parser.add_argument('-v',
|
||||
'--verbose',
|
||||
help="increase output verbosity.",
|
||||
action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
print("Argument:")
|
||||
print(args)
|
||||
|
||||
base_url = "https://raw.githubusercontent.com/matrix-org/matrix-spec/main/data-definitions/sas-emoji.json"
|
||||
|
||||
print("Downloading " + base_url + "…")
|
||||
|
||||
r0 = requests.get(base_url)
|
||||
data0 = json.loads(r0.content.decode())
|
||||
|
||||
if args.verbose:
|
||||
print("Json data:")
|
||||
print(data0)
|
||||
|
||||
print()
|
||||
|
||||
# number -> translation
|
||||
default = dict()
|
||||
# Language -> emoji -> translation
|
||||
cumul = dict()
|
||||
|
||||
for emoji in data0:
|
||||
d = dict()
|
||||
number = emoji["number"]
|
||||
d["description"] = emoji["description"]
|
||||
d["emoji"] = emoji["emoji"]
|
||||
d["unicode"] = emoji["unicode"]
|
||||
if args.verbose:
|
||||
print("Dict: " + str(d))
|
||||
default[number] = d
|
||||
|
||||
for lang in emoji["translated_descriptions"]:
|
||||
if args.verbose:
|
||||
print("Lang: " + lang)
|
||||
if not (lang in cumul):
|
||||
cumul[lang] = dict()
|
||||
d = dict()
|
||||
d["description"] = emoji["translated_descriptions"][lang]
|
||||
cumul[lang][number] = d
|
||||
|
||||
if args.verbose:
|
||||
print(default)
|
||||
print(cumul)
|
||||
|
||||
def write_file(file, dict):
|
||||
print("Writing file " + file)
|
||||
if args.verbose:
|
||||
print("With")
|
||||
print(dict)
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
with open(file, mode="w", encoding="utf8") as o:
|
||||
o.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
|
||||
o.write("<resources>\n")
|
||||
o.write(" <!-- Generated file, do not edit -->\n")
|
||||
for key in dict:
|
||||
if dict[key] is None:
|
||||
continue
|
||||
if dict[key]["description"] is None:
|
||||
continue
|
||||
o.write(" <string name=\"verification_emoji_" + format(key, '02d') + "\">" + dict[key]["description"].replace("'", "\\'") + "</string>\n")
|
||||
o.write("</resources>\n")
|
||||
|
||||
scripts_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
data_defs_dir = os.path.join(scripts_dir, "../../features/verifysession/impl/src/main/res")
|
||||
|
||||
# Write default file
|
||||
write_file(os.path.join(data_defs_dir, "values/strings_sas.xml"), default)
|
||||
|
||||
# Write each language file
|
||||
for lang in cumul:
|
||||
androidLang = lang \
|
||||
.replace("_", "-r") \
|
||||
.replace("zh-rHans", "zh-rCN") \
|
||||
.replace("zh-rHant", "zh-rTW") \
|
||||
.replace("id", "in")
|
||||
write_file(os.path.join(data_defs_dir, "values-" + androidLang + "/strings_sas.xml"), cumul[lang])
|
||||
|
||||
print()
|
||||
print("Success!")
|
||||
101
tools/sdk/build_rust_sdk.sh
Executable file
101
tools/sdk/build_rust_sdk.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# Ask to build from local source or to clone the repository
|
||||
read -p "Do you want to build the Rust SDK from local source (yes/no) default to yes? " buildLocal
|
||||
buildLocal=${buildLocal:-yes}
|
||||
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
date=$(date +%Y%m%d%H%M%S)
|
||||
else
|
||||
date=$(gdate +%Y%m%d%H%M%S)
|
||||
fi
|
||||
|
||||
elementPwd=$(pwd)
|
||||
|
||||
# Ask for the Rust SDK local source path
|
||||
# if folder rustSdk/ exists, use it as default
|
||||
if [ "${buildLocal}" == "yes" ]; then
|
||||
read -p "Please enter the path to the Rust SDK local source, default to ../matrix-rust-sdk" rustSdkPath
|
||||
rustSdkPath=${rustSdkPath:-../matrix-rust-sdk/}
|
||||
if [ ! -d "${rustSdkPath}" ]; then
|
||||
printf "\nFolder ${rustSdkPath} does not exist. Please clone the matrix-rust-sdk repository in the folder ../matrix-rust-sdk.\n\n"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
read -p "Please enter the Rust SDK repository url, default to https://github.com/matrix-org/matrix-rust-sdk.git " rustSdkUrl
|
||||
rustSdkUrl=${rustSdkUrl:-https://github.com/matrix-org/matrix-rust-sdk.git}
|
||||
read -p "Please enter the Rust SDK branch, default to main " rustSdkBranch
|
||||
rustSdkBranch=${rustSdkBranch:-main}
|
||||
cd ..
|
||||
git clone "${rustSdkUrl}" matrix-rust-sdk-"$date"
|
||||
cd matrix-rust-sdk-"$date"
|
||||
git checkout "${rustSdkBranch}"
|
||||
rustSdkPath=$(pwd)
|
||||
cd "${elementPwd}"
|
||||
fi
|
||||
|
||||
|
||||
cd "${rustSdkPath}"
|
||||
git status
|
||||
|
||||
read -p "Will build with this version of the Rust SDK ^. Is it correct (yes/no) default to yes? " sdkCorrect
|
||||
sdkCorrect=${sdkCorrect:-yes}
|
||||
|
||||
if [ "${sdkCorrect}" != "yes" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ask if the user wants to build the app after
|
||||
read -p "Do you want to build the app after (yes/no) default to no? " buildApp
|
||||
buildApp=${buildApp:-no}
|
||||
|
||||
cd "${elementPwd}"
|
||||
|
||||
default_arch="$(uname -m)-linux-android"
|
||||
# On ARM MacOS, `uname -m` returns arm64, but the toolchain is called aarch64
|
||||
default_arch="${default_arch/arm64/aarch64}"
|
||||
|
||||
read -p "Enter the architecture you want to build for (default '$default_arch'): " target_arch
|
||||
target_arch="${target_arch:-${default_arch}}"
|
||||
|
||||
# If folder ../matrix-rust-components-kotlin does not exist, clone the repo
|
||||
if [ ! -d "../matrix-rust-components-kotlin" ]; then
|
||||
printf "\nFolder ../matrix-rust-components-kotlin does not exist. Cloning the repository into ../matrix-rust-components-kotlin.\n\n"
|
||||
git clone https://github.com/matrix-org/matrix-rust-components-kotlin.git ../matrix-rust-components-kotlin
|
||||
fi
|
||||
|
||||
printf "\nResetting matrix-rust-components-kotlin to the latest main branch...\n\n"
|
||||
cd ../matrix-rust-components-kotlin
|
||||
git reset --hard
|
||||
git checkout main
|
||||
git pull
|
||||
|
||||
printf "\nBuilding the SDK for ${target_arch}...\n\n"
|
||||
./scripts/build.sh -p "${rustSdkPath}" -m sdk -t "${target_arch}" -o "${elementPwd}/libraries/rustsdk"
|
||||
|
||||
cd "${elementPwd}"
|
||||
mv ./libraries/rustsdk/sdk-android-debug.aar ./libraries/rustsdk/matrix-rust-sdk.aar
|
||||
mkdir -p ./libraries/rustsdk/sdks
|
||||
cp ./libraries/rustsdk/matrix-rust-sdk.aar ./libraries/rustsdk/sdks/matrix-rust-sdk-"${date}".aar
|
||||
|
||||
|
||||
if [ "${buildApp}" == "yes" ]; then
|
||||
printf "\nBuilding the application...\n\n"
|
||||
./gradlew assembleDebug
|
||||
fi
|
||||
|
||||
if [ "${buildLocal}" == "no" ]; then
|
||||
printf "\nCleaning up...\n\n"
|
||||
rm -rf ../matrix-rust-sdk-"$date"
|
||||
fi
|
||||
|
||||
printf "\nDone!\n"
|
||||
1
tools/templates/FeatureModule.json
Normal file
1
tools/templates/FeatureModule.json
Normal file
@@ -0,0 +1 @@
|
||||
{"template":{"name":"","isDir":true,"placeholders":{"MODULE_NAME":"","FEATURE_NAME":"","BUILD_GRADLE_API":"build.gradle.kts","BUILD_GRADLE_IMPL":"build.gradle.kts"},"fileTemplates":{"${FEATURE_NAME}EntryPoint":"Template Module Feature Entry Point API","Default${FEATURE_NAME}EntryPoint":"Template Module Feature Entry Point Flow Impl","${BUILD_GRADLE_API}":"Template Module Feature Build Gradle API","${BUILD_GRADLE_IMPL}":"Template Module Feature Build Gradle Impl","${FEATURE_NAME}FlowNode":"Template Module Feature Node Flow Impl"},"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"api","isDir":true,"realChildren":[{"name":"src","isDir":true,"realChildren":[{"name":"main","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"api","isDir":true,"realChildren":[{"name":"${FEATURE_NAME}EntryPoint","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]}]}]}]}]}]}]},{"name":"${BUILD_GRADLE_API}","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]},{"name":"impl","isDir":true,"realChildren":[{"name":"src","isDir":true,"realChildren":[{"name":"main","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"impl","isDir":true,"realChildren":[{"name":"Default${FEATURE_NAME}EntryPoint","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]},{"name":"${FEATURE_NAME}FlowNode","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]}]}]}]}]}]},{"name":"test","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"impl","isDir":true,"realChildren":[]}]}]}]}]}]}]}]}]},{"name":"${BUILD_GRADLE_IMPL}","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]},"language":"java","templateName":"FeatureModule","lowercaseDir":true,"capitalizeFile":false,"packageNameToDir":false}
|
||||
0
tools/templates/files/IntelliJ IDEA Global Settings
Normal file
0
tools/templates/files/IntelliJ IDEA Global Settings
Normal file
@@ -0,0 +1,11 @@
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.${MODULE_NAME}.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.${MODULE_NAME}.impl"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
api(projects.features.${MODULE_NAME}.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.element.android.features.${MODULE_NAME}.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
|
||||
interface ${FEATURE_NAME}EntryPoint : FeatureEntryPoint {
|
||||
fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: Callback,
|
||||
): Node
|
||||
|
||||
interface Callback : Plugin {
|
||||
// Add your callbacks
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.element.android.features.${MODULE_NAME}.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.${MODULE_NAME}.api.${FEATURE_NAME}EntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import dev.zacsweers.metro.AppScope
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class Default${FEATURE_NAME}EntryPoint() : ${FEATURE_NAME}EntryPoint {
|
||||
|
||||
override fun createNode(
|
||||
parentNode: Node,
|
||||
buildContext: BuildContext,
|
||||
callback: ${FEATURE_NAME}EntryPoint.Callback,
|
||||
): Node {
|
||||
return parentNode.createNode<${FEATURE_NAME}FlowNode>(buildContext, listOf(callback))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package io.element.android.features.${MODULE_NAME}.impl
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
// CHANGE THE SCOPE
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
class ${FEATURE_NAME}FlowNode(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
) : BackstackNode<${FEATURE_NAME}FlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = NavTarget.Root,
|
||||
savedStateMap = buildContext.savedStateMap,
|
||||
),
|
||||
buildContext = buildContext,
|
||||
plugins = plugins,
|
||||
) {
|
||||
|
||||
sealed interface NavTarget : Parcelable {
|
||||
@Parcelize
|
||||
object Root : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
return when (navTarget) {
|
||||
NavTarget.Root -> {
|
||||
//Give your root node or completely delete this FlowNode if you have only one node.
|
||||
createNode<>(buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
@Inject
|
||||
class ${NAME}Presenter() : Presenter<${NAME}State> {
|
||||
|
||||
@Composable
|
||||
override fun present(): ${NAME}State {
|
||||
|
||||
fun handleEvent(event: ${NAME}Event) {
|
||||
when (event) {
|
||||
${NAME}Event.MyEvent -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
return ${NAME}State(
|
||||
eventSink = ::handleEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
||||
open class ${NAME}StateProvider : PreviewParameterProvider<${NAME}State> {
|
||||
override val values: Sequence<${NAME}State>
|
||||
get() = sequenceOf(
|
||||
a${NAME}State(),
|
||||
// Add other states here
|
||||
)
|
||||
}
|
||||
|
||||
fun a${NAME}State() = ${NAME}State(
|
||||
eventSink = {}
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import io.element.android.annotations.ContributesNode
|
||||
import dev.zacsweers.metro.AppScope
|
||||
|
||||
// CHANGE THE SCOPE
|
||||
@ContributesNode(AppScope::class)
|
||||
@AssistedInject
|
||||
class ${NAME}Node(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: ${NAME}Presenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
${NAME}View(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
@Composable
|
||||
fun ${NAME}View(
|
||||
state: ${NAME}State,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier, contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
"${NAME} feature view",
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ${NAME}ViewPreview(
|
||||
@PreviewParameter(${NAME}StateProvider::class) state: ${NAME}State
|
||||
) = ElementPreview {
|
||||
${NAME}View(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end
|
||||
|
||||
// TODO add your ui models. Remove the eventSink if you don't have events.
|
||||
data class ${NAME}State(
|
||||
val eventSink: (${NAME}Event) -> Unit
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}#end
|
||||
|
||||
// TODO Add your events or remove the file completely if no events
|
||||
sealed interface ${NAME}Event {
|
||||
data object MyEvent: ${NAME}Event
|
||||
}
|
||||
18
tools/templates/files/options/file.template.settings.xml
Normal file
18
tools/templates/files/options/file.template.settings.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<application>
|
||||
<component name="ExportableFileTemplateSettings">
|
||||
<default_templates>
|
||||
<template name="Template Presentation Classes.kt" file-name="${NAME}Presenter" reformat="true" live-template-enabled="false">
|
||||
<template name="Template Presentation Classes.kt.child.0.kt" file-name="${NAME}StateProvider" reformat="true" live-template-enabled="false" />
|
||||
<template name="Template Presentation Classes.kt.child.1.kt" file-name="${NAME}Node" reformat="true" live-template-enabled="false" />
|
||||
<template name="Template Presentation Classes.kt.child.2.kt" file-name="${NAME}View" reformat="true" live-template-enabled="false" />
|
||||
<template name="Template Presentation Classes.kt.child.3.kt" file-name="${NAME}State" reformat="true" live-template-enabled="false" />
|
||||
<template name="Template Presentation Classes.kt.child.4.kt" file-name="${NAME}Event" reformat="true" live-template-enabled="false" />
|
||||
</template>
|
||||
<template name="Template Presentation Classes.kt.child.0.kt" file-name="${NAME}StateProvider" reformat="true" live-template-enabled="false" />
|
||||
<template name="Template Presentation Classes.kt.child.1.kt" file-name="${NAME}Node" reformat="true" live-template-enabled="false" />
|
||||
<template name="Template Presentation Classes.kt.child.2.kt" file-name="${NAME}View" reformat="true" live-template-enabled="false" />
|
||||
<template name="Template Presentation Classes.kt.child.3.kt" file-name="${NAME}State" reformat="true" live-template-enabled="false" />
|
||||
<template name="Template Presentation Classes.kt.child.4.kt" file-name="${NAME}Event" reformat="true" live-template-enabled="false" />
|
||||
</default_templates>
|
||||
</component>
|
||||
</application>
|
||||
17
tools/templates/generate_templates.sh
Executable file
17
tools/templates/generate_templates.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2023-2024 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
echo "Zipping the contents of the 'files' directory..."
|
||||
|
||||
# Ensure tmp folder exists
|
||||
mkdir -p tmp
|
||||
|
||||
rm -f ./tmp/file_templates.zip
|
||||
pushd ./tools/templates/files || exit
|
||||
zip -r ../../../tmp/file_templates.zip .
|
||||
popd || exit
|
||||
38
tools/test/checkInvalidScreenshots.py
Executable file
38
tools/test/checkInvalidScreenshots.py
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import os
|
||||
|
||||
from util import compare
|
||||
|
||||
|
||||
def checkInvalidScreenshots(reference):
|
||||
__doc__ = "Detect invalid screenshot, by comparing to an invalid reference."
|
||||
path_of_screenshots = "tests/uitests/src/test/snapshots/images/"
|
||||
files = os.listdir(path_of_screenshots)
|
||||
counter = 0
|
||||
for file in files:
|
||||
if not compare(reference, path_of_screenshots + file):
|
||||
print("Invalid screenshot detected: " + file)
|
||||
counter += 1
|
||||
return counter
|
||||
|
||||
|
||||
def main():
|
||||
invalid_screenshot_reference_path = "tools/test/invalid_screenshot.png"
|
||||
result = checkInvalidScreenshots(invalid_screenshot_reference_path)
|
||||
if result > 0:
|
||||
print("%d invalid screenshot(s) detected" % result)
|
||||
print("Please check that the Preview is OK in Android Studio. You may want to use a Fake Composable for the screenshot to render correctly.")
|
||||
exit(1)
|
||||
else:
|
||||
print("No invalid screenshot detected!")
|
||||
exit(0)
|
||||
|
||||
|
||||
main()
|
||||
193
tools/test/generateAllScreenshots.py
Executable file
193
tools/test/generateAllScreenshots.py
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
from util import compare
|
||||
|
||||
|
||||
# Read all arguments and return a list of them, this are the languages list.
|
||||
def readArguments():
|
||||
# Return sys.argv without the first argument
|
||||
return sys.argv[1:]
|
||||
|
||||
|
||||
def generateAllScreenshots(languages):
|
||||
# If languages is empty, generate all screenshots
|
||||
if len(languages) == 0:
|
||||
print("Generating all screenshots...")
|
||||
os.system("./gradlew recordPaparazziDebug -PallLanguages")
|
||||
else:
|
||||
tFile = "tests/uitests/src/test/kotlin/translations/TranslationsScreenshotTest.kt"
|
||||
print("Generating screenshots for languages: %s" % languages)
|
||||
# Record the languages one by one, else it's getting too slow
|
||||
for lang in languages:
|
||||
print("Generating screenshots for language: %s" % lang)
|
||||
# Patch file TranslationsScreenshotTest.kt, replace `@TestParameter(value = ["de"])` with `@TestParameter(value = [<the languages>])`
|
||||
with open(tFile, "r") as file:
|
||||
data = file.read()
|
||||
data = data.replace("@TestParameter(value = [\"de\"])", "@TestParameter(value = [\"%s\"])" % lang)
|
||||
with open(tFile, "w") as file:
|
||||
file.write(data)
|
||||
os.system("./gradlew recordPaparazziDebug -PallLanguagesNoEnglish")
|
||||
# Git reset the change on file TranslationsScreenshotTest.kt
|
||||
os.system("git checkout HEAD -- %s" % tFile)
|
||||
|
||||
|
||||
def detectLanguages():
|
||||
__doc__ = "Detect languages from screenshots, other than English"
|
||||
files = os.listdir("tests/uitests/src/test/snapshots/images/")
|
||||
languages = set(map(lambda file: file[-6:-4], files))
|
||||
languages = [lang for lang in languages if re.match("[a-z]", lang) and lang != "en"]
|
||||
print("Detected languages: %s" % languages)
|
||||
return languages
|
||||
|
||||
|
||||
def deleteDuplicatedScreenshots(lang):
|
||||
__doc__ = "Delete screenshots identical to the English version for a language"
|
||||
print("Deleting screenshots identical to the English version for language %s..." % lang)
|
||||
files = os.listdir("tests/uitests/src/test/snapshots/images/")
|
||||
# Filter files by language
|
||||
files = [file for file in files if file[-6:-4] == lang]
|
||||
identicalFileCounter = 0
|
||||
differentFileCounter = 0
|
||||
for file in files:
|
||||
englishFile = file[:-6] + "en" + file[-4:]
|
||||
fullFile = "tests/uitests/src/test/snapshots/images/" + file
|
||||
fullEnglishFile = "tests/uitests/src/test/snapshots/images/" + englishFile
|
||||
isDifferent = compare(fullFile, fullEnglishFile)
|
||||
if isDifferent:
|
||||
differentFileCounter += 1
|
||||
else:
|
||||
identicalFileCounter += 1
|
||||
os.remove(fullFile)
|
||||
print("For language %s, keeping %d files and deleting %d files." % (lang, differentFileCounter, identicalFileCounter))
|
||||
|
||||
|
||||
def moveScreenshots(lang):
|
||||
__doc__ = "Move screenshots to the folder per language"
|
||||
targetFolder = "screenshots/" + lang
|
||||
print("Deleting existing screenshots for %s..." % lang)
|
||||
os.system("rm -rf %s" % targetFolder)
|
||||
print("Moving screenshots for %s to %s..." % (lang, targetFolder))
|
||||
files = os.listdir("tests/uitests/src/test/snapshots/images/")
|
||||
# Filter files by language
|
||||
files = [file for file in files if file[-6:-4] == lang]
|
||||
# Create the folder "./screenshots/<lang>"
|
||||
os.makedirs(targetFolder, exist_ok=True)
|
||||
for file in files:
|
||||
fullFile = "tests/uitests/src/test/snapshots/images/" + file
|
||||
os.rename(fullFile, targetFolder + "/" + file)
|
||||
|
||||
|
||||
def detectRecordedLanguages():
|
||||
# List all the subfolders of the screenshots folder which contains 2 letters, sorted alphabetically
|
||||
return sorted([f for f in os.listdir("screenshots") if len(f) == 2])
|
||||
|
||||
|
||||
def computeDarkFileName(lightFileName):
|
||||
if "_Day" in lightFileName:
|
||||
return lightFileName.replace("_Day", "_Night")
|
||||
match = re.match("(.*)_Day_(\\d+)_(.*)", lightFileName, flags=re.ASCII)
|
||||
if match:
|
||||
return match.group(1) + "_Night_" + match.group(2) + "_" + match.group(3)
|
||||
return ""
|
||||
|
||||
|
||||
def checkForScreenshotNameDuplication():
|
||||
__doc__ = "Check for screenshots name duplication"
|
||||
print("Check for screenshots name duplication...")
|
||||
files = os.listdir("tests/uitests/src/test/snapshots/images/")
|
||||
dict = {}
|
||||
for file in files:
|
||||
start = file.find("_") + 1
|
||||
end = file.find("_", start)
|
||||
screenshotName = file[start:end]
|
||||
if screenshotName in dict:
|
||||
dict[screenshotName].append(file[:end])
|
||||
else:
|
||||
dict[screenshotName] = [file[:end]]
|
||||
error = 0
|
||||
for key in dict:
|
||||
if key in ["Icon", "RoundIcon"]:
|
||||
continue
|
||||
values = set(dict[key])
|
||||
if len(values) > 1:
|
||||
print("Duplicated screenshot name: %s" % key)
|
||||
for value in values:
|
||||
print(" - %s" % value)
|
||||
error += 1
|
||||
if error:
|
||||
print("Warning: %d duplicated screenshot name(s) found" % error)
|
||||
|
||||
|
||||
def generateJavascriptFile():
|
||||
__doc__ = "Generate a javascript file to load the screenshots"
|
||||
print("Generating javascript file...")
|
||||
languages = detectRecordedLanguages()
|
||||
# First item is the list of languages, adding "en" and "en-dark" at the beginning
|
||||
data = [["en", "en-dark"] + languages]
|
||||
files = sorted(
|
||||
os.listdir("tests/uitests/src/test/snapshots/images/"),
|
||||
key=lambda file: file[file.find("_", 1):],
|
||||
)
|
||||
for file in files:
|
||||
# Continue if file contains "_Night", keep only light screenshots
|
||||
if "_Night" in file:
|
||||
continue
|
||||
dataForFile = [file[:-4]]
|
||||
darkFile = computeDarkFileName(file)
|
||||
if os.path.exists("./tests/uitests/src/test/snapshots/images/" + darkFile):
|
||||
dataForFile.append(darkFile[:-4])
|
||||
else:
|
||||
dataForFile.append("")
|
||||
for l in languages:
|
||||
simpleFile = file[:-6] + l
|
||||
translatedFile = "./screenshots/" + l + "/" + simpleFile + ".png"
|
||||
if os.path.exists(translatedFile):
|
||||
# Get the last modified date of the file in seconds and round to days
|
||||
date = os.popen("git log -1 --format=%ct -- \"" + translatedFile + "\"").read().strip()
|
||||
# if date is empty, use today's date
|
||||
if date == "":
|
||||
date = time.time()
|
||||
dateDay = int(date) // 86400
|
||||
dataForFile.append(dateDay)
|
||||
else:
|
||||
dataForFile.append(0)
|
||||
data.append(dataForFile)
|
||||
|
||||
with open("screenshots/html/data.js", "w") as f:
|
||||
f.write("// Generated file, do not edit\n")
|
||||
f.write("export const screenshots = [\n")
|
||||
for line in data:
|
||||
f.write("[")
|
||||
for item in line:
|
||||
# If item is a string, add quotes
|
||||
if isinstance(item, str):
|
||||
f.write("\"" + item + "\",")
|
||||
else:
|
||||
f.write(str(item) + ",")
|
||||
f.write("],\n")
|
||||
f.write("];\n")
|
||||
|
||||
|
||||
def main():
|
||||
checkForScreenshotNameDuplication()
|
||||
generateAllScreenshots(readArguments())
|
||||
lang = detectLanguages()
|
||||
for l in lang:
|
||||
deleteDuplicatedScreenshots(l)
|
||||
moveScreenshots(l)
|
||||
generateJavascriptFile()
|
||||
|
||||
|
||||
main()
|
||||
32
tools/test/generateWorldScreenshots.py
Executable file
32
tools/test/generateWorldScreenshots.py
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
def detectAllExistingTranslations():
|
||||
# Read all the folder in "libraries/ui-strings/src/main/res"
|
||||
folders = os.listdir("libraries/ui-strings/src/main/res")
|
||||
# Remove the "values" folder
|
||||
folders.remove("values")
|
||||
# Map to keep only the language code
|
||||
folders = list(map(lambda folder: folder[7:], folders))
|
||||
# Map to keep only the string before the "-"
|
||||
folders = list(map(lambda folder: folder.split("-")[0], folders))
|
||||
# Remove duplicates
|
||||
folders = list(set(folders))
|
||||
return folders
|
||||
|
||||
|
||||
def main():
|
||||
languages = detectAllExistingTranslations()
|
||||
print ("Will record the screenshots for those languages: %s" % languages)
|
||||
# Run the python script "generateAllScreenshots.py" with the detected languages
|
||||
os.system("./tools/test/generateAllScreenshots.py %s" % " ".join(languages))
|
||||
|
||||
main()
|
||||
BIN
tools/test/invalid_screenshot.png
Normal file
BIN
tools/test/invalid_screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
23
tools/test/util.py
Normal file
23
tools/test/util.py
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2025 Element Creations Ltd.
|
||||
# Copyright 2024, 2025 New Vector Ltd.
|
||||
#
|
||||
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
# Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def compare(file1, file2):
|
||||
__doc__ = "Compare two files, return True if different, False if identical."
|
||||
# Compare file size
|
||||
file1_stats = os.stat(file1)
|
||||
file2_stats = os.stat(file2)
|
||||
if file1_stats.st_size != file2_stats.st_size:
|
||||
return True
|
||||
# Compare file content
|
||||
with open(file1, "rb") as f1, open(file2, "rb") as f2:
|
||||
content1 = f1.read()
|
||||
content2 = f2.read()
|
||||
return content1 != content2
|
||||
Reference in New Issue
Block a user