diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8c8d67b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Plugin unit tests + run: ./gradlew :plugin:test :plugin:jacocoTestReport :plugin:jacocoTestCoverageVerification :plugin:detekt --no-daemon + + - name: Plugin integration tests (optional) + continue-on-error: true + run: ./gradlew :plugin:integrationTest --no-daemon + + - name: Sample app assembleDebug + run: ./gradlew :app:assembleDebug --no-daemon + + - name: Performance non-regression check (assembleDebug) + env: + PERF_MAX_SECONDS: "240" + run: | + ./gradlew :app:clean --no-daemon + ./gradlew :app:assembleDebug --no-daemon + start=$(date +%s) + ./gradlew :app:assembleDebug --no-daemon + end=$(date +%s) + elapsed=$((end - start)) + echo "Measured assembleDebug (warm) time: ${elapsed}s" + if [ "$elapsed" -gt "$PERF_MAX_SECONDS" ]; then + echo "Performance regression: ${elapsed}s > ${PERF_MAX_SECONDS}s" + exit 1 + fi + + agp-compat: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + agp-version: ["8.0.2", "8.7.3", "9.0.0"] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Set AGP version in version catalog + run: sed -i.bak 's/^agp = ".*"/agp = "${{ matrix.agp-version }}"/' gradle/libs.versions.toml + + - name: Set Gradle version for selected AGP + run: | + if [ "${{ matrix.agp-version }}" = "9.0.0" ]; then + sed -i.bak 's#^distributionUrl=.*#distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip#' gradle/wrapper/gradle-wrapper.properties + else + sed -i.bak 's#^distributionUrl=.*#distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip#' gradle/wrapper/gradle-wrapper.properties + fi + + - name: AGP compatibility check (assembleDebug) + run: ./gradlew :app:assembleDebug --no-daemon diff --git a/.github/workflows/copilot_commit.yml b/.github/workflows/copilot_commit.yml index 2b5387a..dc96e1a 100644 --- a/.github/workflows/copilot_commit.yml +++ b/.github/workflows/copilot_commit.yml @@ -14,6 +14,9 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} - uses: vypdev/copilot@v2 with: diff --git a/.github/workflows/copilot_issue.yml b/.github/workflows/copilot_issue.yml index 4c1b9b4..7cf4cb6 100644 --- a/.github/workflows/copilot_issue.yml +++ b/.github/workflows/copilot_issue.yml @@ -11,6 +11,9 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} - uses: vypdev/copilot@v2 with: diff --git a/.github/workflows/copilot_issue_comment.yml b/.github/workflows/copilot_issue_comment.yml index 0c23c36..f19d834 100644 --- a/.github/workflows/copilot_issue_comment.yml +++ b/.github/workflows/copilot_issue_comment.yml @@ -13,6 +13,9 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} - uses: vypdev/copilot@v2 with: diff --git a/.github/workflows/copilot_pull_request.yml b/.github/workflows/copilot_pull_request.yml index ccf1288..6b11c2d 100644 --- a/.github/workflows/copilot_pull_request.yml +++ b/.github/workflows/copilot_pull_request.yml @@ -11,6 +11,9 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} - uses: vypdev/copilot@v2 with: diff --git a/.github/workflows/copilot_pull_request_comment.yml b/.github/workflows/copilot_pull_request_comment.yml index a521f9e..c11bf74 100644 --- a/.github/workflows/copilot_pull_request_comment.yml +++ b/.github/workflows/copilot_pull_request_comment.yml @@ -13,6 +13,9 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} - uses: vypdev/copilot@v2 with: diff --git a/.github/workflows/hotfix_workflow.yml b/.github/workflows/hotfix_workflow.yml index e631b92..a888abc 100644 --- a/.github/workflows/hotfix_workflow.yml +++ b/.github/workflows/hotfix_workflow.yml @@ -30,6 +30,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} - name: Validate inputs env: @@ -77,6 +80,9 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} - name: Copilot - Create Tag uses: vypdev/copilot@v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f1b4785 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +# Manual / tag-driven publish to Maven Central (requires signing + Nexus credentials). +name: Publish + +on: + workflow_dispatch: + inputs: + dry_run: + description: "If true, only build plugin (no upload)" + required: false + default: "true" + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build plugin JAR + run: ./gradlew :plugin:jar --no-daemon + + # Wire signing + publish when secrets are configured: + # ./gradlew :plugin:publishPluginPublicationToSonatypeRepository -PnexusUsername=... -PnexusPassword=... diff --git a/.github/workflows/release_workflow.yml b/.github/workflows/release_workflow.yml index 7baf951..e193a53 100644 --- a/.github/workflows/release_workflow.yml +++ b/.github/workflows/release_workflow.yml @@ -141,6 +141,9 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT }} - name: Copilot - Create Tag uses: vypdev/copilot@v2 diff --git a/.gitmodules b/.gitmodules index 82ad0a6..df94062 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,4 +2,7 @@ # If the repo URL is not accessible, place the JNI library at stringcare-jni/ (copy from ../stringcare-android-c) or ensure library/CMakeLists.txt path is correct. [submodule "stringcare-jni"] path = stringcare-jni - url = git@github.com:vypdev/stringcare-android-c.git + url = https://github.com/vypdev/stringcare-android-c.git +[submodule "example"] + path = example + url = https://github.com/vypdev/stringcare-android-sample.git diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..11c76da --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,46 @@ +# Changelog + +## [Unreleased] — refactor / 6.x groundwork + +### Added + +- **Docs**: [AGP generated resources](docs/agp-generated-resources.md) — rationale for `addGeneratedSourceDirectory`. +- **AGP integration**: Per-variant obfuscation outputs are registered with `Sources.addGeneratedSourceDirectory` (`res` and `assets`) so merges consume `build/intermediates/stringcare/generated-*` overlays without touching `src/`. + +### Changed + +- **Breaking — build pipeline**: Removed in-place source mutation and post-merge restore. Tasks renamed to `stringcareObfuscateStringResources` and `stringcareObfuscateAssets`. Removed `stringcareAfterMerge*` / temp backup tree / `Restore*Task` / `RestoreFilesUseCase`. +- **Gradle**: Obfuscate tasks are cache-friendly (`@DisableCachingByDefault` removed); proper `@InputFiles` + `@OutputDirectory` boundaries. +- **Runtime behaviour**: Same APK obfuscation semantics; developer checkout and IDE always see plaintext resources in source control. + +### Changed (historical notes) + +- **Architecture**: Domain models under `domain.models` (with `models` typealiases for compatibility); `infrastructure` packages for parsers, Gradle wiring, crypto, filesystem; thin `ObfuscateStringsUseCase` + `ResourceRepository`. +- **XML**: SAX-first `strings.xml` parsing with DOM fallback for nested markup; `XmlAttributes` / `XmlParser` facade. +- **Tasks**: Gradle `Property` inputs on obfuscate/preview tasks. +- **AGP**: `compileOnly` for the Android Gradle Plugin dependency (provided at runtime on Android projects). +- **JAR size**: With `compileOnly` AGP, plugin JAR is ~180KB vs previous multi‑MB fat jar (verify locally with `./gradlew :plugin:jar`). +- **Gradle**: `StringCareBuildService` (shared build service) replaces mutable static state on `StringCarePlugin` for configuration and variant applicationIds. +- **Dependencies**: Removed Guava and Gson; added `kotlinx-serialization-json` for task JSON list inputs. +- **Execution**: Shell commands use `ProcessBuilder` with a 60s timeout and structured `ExecutionResult` (`Success` / `Failure` / `Timeout`). +- **Native host libs**: SHA-256 verification before `System.load`, retries, optional verbose logging tied to `debug` in tasks. +- **XML / scan**: Faster attribute iteration in `parseXML`; `walkTopDown` skips `build/`, `.gradle/`, `.git/`, `node_modules/`; `mapNotNull` for resource/asset discovery; idempotent `StringCareConfiguration.normalize()`. +- **Tooling**: Detekt + baseline, ktlint (non-failing), JaCoCo hook, Develocity build scan terms in root `settings.gradle.kts`. +- **Tests**: `ObfuscationServiceTest` (JNI roundtrip when loaded), UTF-16 / malformed XML parser cases; CI runs `jacocoTestCoverageVerification` with a low interim line threshold. + +### Breaking changes + +- **Task names / pipeline**: Integrations that referenced `stringcareBeforeMerge*` / `stringcareAfterMerge*` or depended on restore-side effects must use `stringcareObfuscateStringResources*` / `stringcareObfuscateAssets*` instead. +- **Internal APIs**: Static mutable state on `StringCarePlugin` (paths, temp folder, variant map) was removed in favor of `StringCareBuildService`. Any build logic or tests reaching into those internals must use task inputs / the registered build service instead. +- **Dependencies on the plugin JAR**: Guava and Gson are no longer bundled; list-style DSL fields are serialized with Kotlin serialization in task properties. Pure-Java consumers of internal packages are unsupported. + +### Performance (roadmap vs previous generations) + +- XML handling targets **SAX-first** parsing with DOM only when nested `` markup requires it, and filesystem walks **prune** heavy directories (`build/`, `.gradle/`, `.git/`, `node_modules/`). End-to-end timings depend on project size; run `./gradlew :plugin:test` and your app’s obfuscation tasks locally to validate. + +### Migration notes + +- Public plugin id and DSL block name are unchanged (`dev.vyp.stringcare.plugin`, `stringcare { }`). +- If you relied on internal APIs (`StringCarePlugin.absoluteProjectPath`, etc.), migrate to the build service or task inputs only. + +See [MIGRATION.md](MIGRATION.md). diff --git a/MIGRATION.md b/MIGRATION.md index 5491223..5953eef 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,72 +1,24 @@ -# Migration from StringCare 4.x to 5.0 +# Migration guide (5.x → current refactor) -## GroupId and artifact IDs +## For consumers of the published plugin -- **Old:** `io.github.stringcare` (or `com.stringcare`) -- **New:** `dev.vyp.stringcare` - -Update dependencies and plugin coordinates: - -| 4.x | 5.0 | -|-----|-----| -| `io.github.stringcare:library` | `dev.vyp.stringcare:library` | -| `com.stringcare` plugin ID | `dev.vyp.stringcare.plugin` | - -## Gradle (Kotlin DSL) - -**Before (4.x, Groovy):** - -```groovy -buildscript { - dependencies { - classpath 'com.stringcare:gradle-plugin:4.x' - } -} -apply plugin: 'com.stringcare' -dependencies { - implementation 'io.github.stringcare:library:4.x' -} -``` - -**After (5.0, Kotlin DSL):** +No changes are required in `build.gradle.kts` if you only use the public DSL: ```kotlin -plugins { - id("dev.vyp.stringcare.plugin") -} -dependencies { - implementation("dev.vyp.stringcare:library:5.0.0") +stringcare { + debug = true + // ... } ``` -Resolve the plugin from Maven Central or a local `includeBuild`; see [README](README.md#installation-kotlin-dsl). +## For forks or code that used internal statics -## Plugin configuration - -- Extension and task names are unchanged (`stringcare { ... }`, `stringFiles`, `assetsFiles`, `srcFolders`, `debug`, `skip`). -- AGP 8.x and Gradle 8.x are required; the plugin uses the Variant API. - -## Library package - -- **Old:** `com.stringcare.library` -- **New:** `dev.vyp.stringcare.library` - -Update imports in your app: - -```kotlin -// Before -import com.stringcare.library.SC -import com.stringcare.library.SCTextView - -// After -import dev.vyp.stringcare.library.SC -import dev.vyp.stringcare.library.SCTextView -``` - -## API compatibility - -Public API (e.g. `SC.reveal()`, `SC.obfuscate()`, `SCTextView`, resources usage) is unchanged. Only package and Maven coordinates differ. +| Old | New | +|-----|-----| +| `StringCarePlugin.absoluteProjectPath` | Resolved per build via `StringCareBuildService.absoluteProjectPath()` (internal) | +| `StringCarePlugin.variantMap` | `StringCareBuildService` variant map (internal) | +| `StringCarePlugin.resetFolder()` | Test hook only: clears `StringCareSession` temp root for integration tests (no production temp backup tree) | -## Build / CI +## Native libraries -This repo uses the JNI native library as a Git submodule. Clone with `git clone --recurse-submodules` or `git submodule update --init --recursive`. In GitHub Actions use `checkout` with `submodules: true`. +When updating bundled `.dylib` / `.so` / `.dll`, refresh SHA-256 entries in `Stark.kt` (`EXPECTED_SHA256`). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6fd5209 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security + +## Native binaries + +Host-side JNI libraries shipped in the plugin JAR are verified with SHA-256 before load. If you replace prebuilts, update the expected hashes in `plugin/.../internal/Stark.kt`. + +## Signing fingerprints + +The plugin derives keys from `signingReport` output. Treat CI logs as sensitive if debug logging prints certificate details. + +## Reporting + +Report security issues via the repository’s issue tracker or maintainer contact in the published POM. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f5c98a8..fa63c52 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) id("dev.vyp.stringcare.plugin") } @@ -54,6 +55,14 @@ android { jvmTarget = "17" } + buildFeatures { + compose = true + } + + androidResources { + noCompress += "json" + } + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" @@ -67,6 +76,15 @@ dependencies { exclude(group = "com.android.support", module = "support-annotations") } implementation(libs.androidx.appcompat) + implementation(libs.androidx.core) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + debugImplementation("androidx.compose.ui:ui-tooling") testImplementation("junit:junit:4.13.2") implementation("org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}") implementation("commons-io:commons-io:2.15.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3ab71c6..4403444 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ - + (R.id.programmatically_obfuscation) as TextView).text = message - val numbers = - getString(R.string.pattern, "hi", 3) + " is " + reveal(R.string.pattern, "hi", 3) - (findViewById(R.id.pattern) as TextView).text = numbers - val tvAuto = findViewById(R.id.auto_tv) - findViewById(R.id.btn_change).setOnClickListener { v: View? -> - if (tvAuto.isHtmlEnabled) { - tvAuto.setHtmlSupport(!tvAuto.isHtmlEnabled) - } else if (tvAuto.isRevealingValue) { - tvAuto.setRevealed(!tvAuto.isRevealingValue) - } else if (!tvAuto.isRevealingValue) { - tvAuto.setRevealed(!tvAuto.isRevealingValue) - tvAuto.setHtmlSupport(!tvAuto.isHtmlEnabled) + enableEdgeToEdge() + setContent { + StringObfuscatorSampleTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + StringCareSampleScreen( + modifier = + Modifier + .fillMaxSize() + .padding(innerPadding), + ) + } } } - val equals = reveal(R.string.hello_world_b) == getString(R.string.hello_world_a) - val areEquals = "Same result: $equals" - (findViewById(R.id.same_value) as TextView).text = areEquals - val jsonObjectName = R.string.asset_json_file.reveal() + } +} +@Composable +private fun StringCareSampleScreen(modifier: Modifier = Modifier) { + val context = LocalContext.current - findViewById(R.id.json_object).text = jsonObjectName.json().toString() - findViewById(R.id.json_object_original).text = - String(jsonObjectName.bytes { false }) + val snakePassword = remember { context.getString(R.string.snake_msg_hidden) } + val snakeOriginal = remember { + runCatching { dev.vyp.stringcare.library.SC.reveal(snakePassword, Version.V3) } + .getOrDefault("error") + } + val programmaticMessage = + remember { + "Snake, the password is $snakePassword$snakeOriginal" + } + val patternDemo = + remember { + context.getString(R.string.pattern, "hi", 3) + + " is " + + runCatching { R.string.pattern.reveal("hi", 3) }.getOrDefault("error") + } - val jsonArrayName = R.string.asset_json_raw_file.reveal() - findViewById(R.id.json_array).text = jsonArrayName.jsonArray().toString() - findViewById(R.id.json_array_original).text = - jsonArrayName.bytes { false }.toString() + val sameHello = + remember { + runCatching { R.string.hello_world_b.reveal() == context.getString(R.string.hello_world_a) } + .getOrDefault(false) + } + + val jsonObjectPath = remember { runCatching { R.string.asset_json_file.reveal() }.getOrDefault("") } + val jsonObjectPretty = + remember(jsonObjectPath) { + runCatching { jsonObjectPath.json().toString(2) }.getOrDefault("error") + } + val jsonObjectRaw = + remember(jsonObjectPath) { + runCatching { String(jsonObjectPath.bytes { false }) }.getOrDefault("error") + } + + val jsonArrayPath = + remember { runCatching { R.string.asset_json_raw_file.reveal() }.getOrDefault("") } + val jsonArrayPretty = + remember(jsonArrayPath) { + runCatching { jsonArrayPath.jsonArray().toString(2) }.getOrDefault("error") + } + val jsonArrayRaw = + remember(jsonArrayPath) { + runCatching { jsonArrayPath.bytes { false }.toString() }.getOrDefault("error") + } + + val helloTvHolder = remember { mutableStateOf(null) } + + Column( + modifier = + modifier + .verticalScroll(rememberScrollState()) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = context.getString(R.string.app_name), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + Text( + text = "StringCare sample (Compose): reveals, patterns, assets, and SCTextView.", + style = MaterialTheme.typography.bodyMedium, + ) + + DemoCard(title = context.getString(R.string.html_treatment), color = Color(0xFFE3F2FD)) { + AndroidView( + modifier = Modifier.fillMaxWidth(), + factory = { ctx -> + LayoutInflater.from(ctx).inflate(R.layout.sc_text_hello, null, false) as SCTextView + }, + update = { tv -> helloTvHolder.value = tv }, + ) + TextButton( + onClick = { + val tvAuto = helloTvHolder.value ?: return@TextButton + if (tvAuto.isHtmlEnabled) { + tvAuto.setHtmlSupport(!tvAuto.isHtmlEnabled) + } else if (tvAuto.isRevealingValue) { + tvAuto.setRevealed(!tvAuto.isRevealingValue) + } else if (!tvAuto.isRevealingValue) { + tvAuto.setRevealed(!tvAuto.isRevealingValue) + tvAuto.setHtmlSupport(!tvAuto.isHtmlEnabled) + } + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = "Change") + } + } + + DemoCard( + title = context.getString(R.string.programmatically_obfuscation), + color = Color(0xFFFFF3E0), + ) { + DemoRow(label = "Message", value = programmaticMessage) + } + + DemoCard(title = context.getString(R.string.patterns), color = Color(0xFFF3E5F5)) { + DemoRow(label = "Pattern demo", value = patternDemo) + } + + DemoCard(title = context.getString(R.string.long_new_line_comparison), color = Color(0xFFE8F5E9)) { + DemoRow(label = "Same result", value = sameHello.toString()) + HorizontalDivider(Modifier.padding(vertical = 8.dp)) + Text( + text = context.getString(R.string.hello_world_a), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + AndroidView( + modifier = Modifier.fillMaxWidth(), + factory = { ctx -> + LayoutInflater.from(ctx).inflate(R.layout.sc_text_hello_world_b, null, false) as SCTextView + }, + ) + } + + DemoCard(title = context.getString(R.string.string_resource_disabling_android_treatment), color = Color(0xFFFFEBEE)) { + AndroidView( + modifier = Modifier.fillMaxWidth(), + factory = { ctx -> + LayoutInflater.from(ctx).inflate(R.layout.sc_text_hello_world_c, null, false) as SCTextView + }, + ) + } + + DemoCard(title = context.getString(R.string.json_object_asset), color = Color(0xFFE0F7FA)) { + DemoRow(label = "Path (revealed)", value = jsonObjectPath) + HorizontalDivider(Modifier.padding(vertical = 6.dp)) + DemoRow(label = "JSON", value = jsonObjectPretty) + DemoRow(label = "Raw bytes as String", value = jsonObjectRaw) + } + + DemoCard(title = context.getString(R.string.json_array_asset), color = Color(0xFFF1F8E9)) { + DemoRow(label = "Path (revealed)", value = jsonArrayPath) + HorizontalDivider(Modifier.padding(vertical = 6.dp)) + DemoRow(label = "JSONArray", value = jsonArrayPretty) + DemoRow(label = "Raw bytes", value = jsonArrayRaw) + } + } +} + +@Composable +private fun DemoCard( + title: String, + color: Color, + content: @Composable () -> Unit, +) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = color), + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + content() + } + } +} +@Composable +private fun DemoRow( + label: String, + value: String, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = "$label:", + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) + Text( + text = value, + modifier = Modifier.weight(2f), + style = MaterialTheme.typography.bodySmall, + ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/efraespada/stringobfuscator/StringCareApp.kt b/app/src/main/java/com/efraespada/stringobfuscator/StringCareApp.kt new file mode 100644 index 0000000..9c0b2e5 --- /dev/null +++ b/app/src/main/java/com/efraespada/stringobfuscator/StringCareApp.kt @@ -0,0 +1,11 @@ +package com.efraespada.stringobfuscator + +import android.app.Application +import dev.vyp.stringcare.library.SC + +class StringCareApp : Application() { + override fun onCreate() { + super.onCreate() + SC.init { applicationContext } + } +} diff --git a/app/src/main/java/com/efraespada/stringobfuscator/ui/theme/Theme.kt b/app/src/main/java/com/efraespada/stringobfuscator/ui/theme/Theme.kt new file mode 100644 index 0000000..18ba0fe --- /dev/null +++ b/app/src/main/java/com/efraespada/stringobfuscator/ui/theme/Theme.kt @@ -0,0 +1,13 @@ +package com.efraespada.stringobfuscator.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +@Composable +fun StringObfuscatorSampleTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = lightColorScheme(), + content = content, + ) +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100755 index 1f816d3..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - -