diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
deleted file mode 100644
index 75e81553..00000000
--- a/.github/workflows/android.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: Codeboard Android CI New
-
-on: [push]
-
-jobs:
- build:
- runs-on: windows-latest
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Java 17
- uses: actions/setup-java@v4
- with:
- distribution: 'temurin'
- java-version: '17'
-
- - name: Cache Gradle files
- uses: actions/cache@v3
- with:
- path: |
- ~/.gradle/caches
- ~/.gradle/wrapper
- key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-
-
- - name: Build with Gradle
- run: |
- ./gradlew.bat assemble --stacktrace
-
- - name: Upload APKs
- uses: actions/upload-artifact@v4
- with:
- name: apk
- path: app/build/outputs/apk/
- retention-days: 1
diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml
new file mode 100644
index 00000000..c0ea5e54
--- /dev/null
+++ b/.github/workflows/debug.yml
@@ -0,0 +1,41 @@
+name: Android Debug CI (PR)
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ branches: ['**']
+
+ workflow_dispatch:
+
+concurrency:
+ group: pr-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Set up Java 21
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v6
+
+ - name: Build Debug APK
+ run: ./gradlew assembleDebug --stacktrace
+ env:
+ GITHUB_RUN_NUMBER: ${{ github.run_number }}
+
+ - name: Upload Debug APK
+ uses: actions/upload-artifact@v7
+ with:
+ name: app-debug-apk
+ path: app/build/outputs/apk/debug/*.apk
+ retention-days: 60
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..48c4c174
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,38 @@
+name: Android Release CI
+
+on:
+ push:
+ branches: [ main, master ]
+
+ workflow_dispatch:
+
+concurrency:
+ group: Release
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Set up Java 21
+ uses: actions/setup-java@v5
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v6
+
+ - name: Build Release APK
+ run: ./gradlew assembleRelease --stacktrace
+
+ - name: Upload Release APK
+ uses: actions/upload-artifact@v7
+ with:
+ name: app-release-apk
+ path: app/build/outputs/apk/release/*.apk
+ retention-days: 60
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
deleted file mode 100644
index 0c0c3383..00000000
--- a/.idea/deploymentTargetDropDown.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
deleted file mode 100644
index f8051a6f..00000000
--- a/.idea/migrations.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index e42c2551..00000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,73 +0,0 @@
-apply plugin: 'com.android.application'
-
-android {
- namespace "com.gazlaws.codeboard"
- compileSdkVersion 34
- buildToolsVersion '34.0.0'
-
- defaultConfig {
- applicationId "com.gazlaws.codeboard"
- minSdkVersion 23
- targetSdk 35
- versionCode 23
- versionName "6.0.3"
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- testOptions {
- unitTests.includeAndroidResources = true
- }
- // Gradle automatically adds 'android.test.runner' as a dependency.
- useLibrary 'android.test.runner'
-
- useLibrary 'android.test.base'
- useLibrary 'android.test.mock'
- lint {
- checkReleaseBuilds false
- }
-}
-
-repositories {
- google()
- mavenCentral()
- maven { url 'https://jitpack.io' }
-}
-
-dependencies {
-
- api 'androidx.appcompat:appcompat:1.7.0'
- api 'androidx.legacy:legacy-support-v13:1.0.0'
- api 'androidx.preference:preference:1.2.1'
- api 'androidx.preference:preference:1.2.1'
-
- implementation 'com.google.android.material:material:1.12.0'
-
- //https://github.com/AppIntro/AppIntro
- implementation 'com.github.AppIntro:AppIntro:6.3.1'
-
- //Colour picker
- //implementation 'com.pes.materialcolorpicker:library:1.2.5'
- implementation 'com.github.evilbunny2008:android-material-color-picker-dialog:1.3.7'
-
- // Required for local unit tests (JUnit 4 framework)
- testImplementation 'junit:junit:4.13.2'
-
- // Required for instrumented tests
- implementation 'androidx.annotation:annotation:1.9.1'
- androidTestImplementation 'androidx.annotation:annotation:1.9.1'
-
-
- // Core library
- androidTestImplementation 'androidx.test:core:1.6.1'
-
- // AndroidJUnitRunner and JUnit Rules
- androidTestImplementation 'androidx.test:runner:1.6.2'
-
- androidTestImplementation 'androidx.test.ext:junit:1.2.1'
-
-}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 00000000..67788280
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,57 @@
+plugins {
+ id("com.android.application")
+}
+
+android {
+ namespace = "com.gazlaws.codeboard"
+ compileSdk = 37
+
+ defaultConfig {
+ applicationId = "com.gazlaws.codeboard"
+ minSdk = 23
+ targetSdk = 37
+ versionCode = 24
+ versionName = "7.0.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ debug {
+ applicationIdSuffix = ".debug"
+ versionNameSuffix =
+ System.getenv("GITHUB_RUN_NUMBER")
+ ?.let { "-debug-$it" }
+ ?: "-debug"
+
+ }
+ release {
+ // Sign release build with debug key (if release not available) so it's installable from CI
+ val releaseConfig = signingConfigs.findByName("release")
+ signingConfig = releaseConfig ?: signingConfigs.getByName("debug")
+ isMinifyEnabled = true
+ isShrinkResources = true
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+ }
+}
+
+dependencies {
+ implementation("androidx.appcompat:appcompat:1.7.1")
+ implementation("androidx.preference:preference:1.2.1")
+ implementation("com.google.android.material:material:1.12.0")
+ implementation("com.github.AppIntro:AppIntro:6.3.1")
+ implementation("com.github.evilbunny2008:android-material-color-picker-dialog:1.3.7")
+
+ testImplementation("junit:junit:4.13.2")
+ implementation("androidx.annotation:annotation:1.10.0")
+ androidTestImplementation("androidx.annotation:annotation:1.10.0")
+ androidTestImplementation("androidx.test:core:1.7.0")
+ androidTestImplementation("androidx.test:runner:1.7.0")
+ androidTestImplementation("androidx.test.ext:junit:1.3.0")
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 5e57c3c1..c09ed43e 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,17 +1,55 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in C:\Users\Ruby\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
+# Project-specific ProGuard rules for Codeboard
+# These rules ensure that essential classes are kept to prevent installation and runtime issues.
+
+# Keep all project classes to ensure the IME and main activities function correctly
+-keep class com.gazlaws.codeboard.** { *; }
+
+# Keep Input Method Service and its internal implementations
+-keep class * extends android.inputmethodservice.InputMethodService
+-keep class * extends android.inputmethodservice.InputMethodService$InputMethodImpl
+
+# AndroidX and Material Design components keep rules (Prevents issues with inflation and reflection)
+-keep class androidx.appcompat.** { *; }
+-keep class com.google.android.material.** { *; }
+-keep class androidx.preference.** { *; }
+-keep class androidx.annotation.** { *; }
+-keep class androidx.core.** { *; }
+
+# External Libraries: AppIntro and Color Picker
+-keep class com.github.appintro.** { *; }
+-keep class com.github.evilbunny2008.** { *; }
+
+# Maintain standard Android component entry points
+-keepattributes *Annotation*, Signature, SourceFile, LineNumberTable
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+
+# Support for Javascript interfaces if utilized in WebViews
+-keepclassmembers class * {
+ @android.webkit.JavascriptInterface ;
+}
+
+# Preserve R class for resource access via reflection or dynamic lookup
+-keep class **.R$* { *; }
+
+# Maintain View constructors and setters for XML layout inflation
+-keepclassmembers class * extends android.view.View {
+ public (android.content.Context);
+ public (android.content.Context, android.util.AttributeSet);
+ public (android.content.Context, android.util.AttributeSet, int);
+ public void set*(...);
+}
+
+# Kotlin metadata preservation for hybrid interoperability
+-keep class kotlin.Metadata { *; }
+-dontwarn kotlin.**
+-dontwarn org.jetbrains.kotlin.**
+
+# Suppress warnings from common libraries that may be safely ignored during minification
+-dontwarn androidx.**
+-dontwarn com.google.android.material.**
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 00000000..e500739c
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ CodeBoard Debug
+ Debug CodeBoard IME
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 26c37854..f1ca5d4e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,49 +1,47 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/gazlaws/codeboard/CodeBoardIME.java b/app/src/main/java/com/gazlaws/codeboard/CodeBoardIME.java
index 6fbba24f..d7f4c762 100644
--- a/app/src/main/java/com/gazlaws/codeboard/CodeBoardIME.java
+++ b/app/src/main/java/com/gazlaws/codeboard/CodeBoardIME.java
@@ -1,6 +1,7 @@
package com.gazlaws.codeboard;
import android.Manifest;
+import androidx.core.content.ContextCompat;
import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -707,11 +708,7 @@ private void setNotification(boolean visible) {
mNotificationReceiver = new NotificationReceiver(this);
final IntentFilter pFilter = new IntentFilter(NotificationReceiver.ACTION_SHOW);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- registerReceiver(mNotificationReceiver, pFilter, Context.RECEIVER_NOT_EXPORTED);
- } else {
- registerReceiver(mNotificationReceiver, pFilter);
- }
+ ContextCompat.registerReceiver(this, mNotificationReceiver, pFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
Intent imeIntent = new Intent(NotificationReceiver.ACTION_SHOW);
diff --git a/app/src/main/java/com/gazlaws/codeboard/SettingsFragment.java b/app/src/main/java/com/gazlaws/codeboard/SettingsFragment.java
index 316ad678..a66453a9 100644
--- a/app/src/main/java/com/gazlaws/codeboard/SettingsFragment.java
+++ b/app/src/main/java/com/gazlaws/codeboard/SettingsFragment.java
@@ -7,18 +7,23 @@
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.database.Cursor;
import android.graphics.Color;
+import android.net.Uri;
import android.os.Bundle;
+import android.provider.OpenableColumns;
import android.provider.Settings;
import android.text.InputType;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
+import android.widget.Toast;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.preference.EditTextPreference;
-import androidx.preference.EditTextPreferenceDialogFragmentCompat;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
@@ -26,23 +31,41 @@
import com.gazlaws.codeboard.theme.IOnFocusListenable;
import com.gazlaws.codeboard.theme.ThemeDefinitions;
import com.gazlaws.codeboard.theme.ThemeInfo;
-//import com.pes.androidmaterialcolorpickerdialog.ColorPicker;
-//import com.pes.androidmaterialcolorpickerdialog.ColorPickerCallback;
import com.github.evilbunny2008.androidmaterialcolorpickerdialog.ColorPicker;
import com.github.evilbunny2008.androidmaterialcolorpickerdialog.ColorPickerCallback;
-import static android.provider.Settings.Secure.DEFAULT_INPUT_METHOD;
+import java.io.InputStream;
+import java.io.OutputStream;
+import static android.provider.Settings.Secure.DEFAULT_INPUT_METHOD;
public class SettingsFragment extends PreferenceFragmentCompat implements IOnFocusListenable {
+
KeyboardPreferences keyboardPreferences;
+ private final ActivityResultLauncher exportLauncher = registerForActivityResult(
+ new ActivityResultContracts.CreateDocument("application/x-codeboard"),
+ uri -> {
+ if (uri != null) {
+ performExport(uri);
+ }
+ }
+ );
+
+ private final ActivityResultLauncher importLauncher = registerForActivityResult(
+ new ActivityResultContracts.OpenDocument(),
+ uri -> {
+ if (uri != null) {
+ validateAndPerformImport(uri);
+ }
+ }
+ );
+
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.preferences, rootKey);
- keyboardPreferences = new KeyboardPreferences(requireActivity());
+ keyboardPreferences = new KeyboardPreferences(getActivity());
- // Declare a new thread to do a preference check
Thread t = new Thread(new Runnable() {
@Override
public void run() {
@@ -55,44 +78,101 @@ public void run() {
});
t.start();
- //Only allow numbers
+ //Only allow positive numbers
String[] numberOnlyPrefereces = {"vibrate_ms", "font_size", "size_portrait", "size_landscape"};
for (String key : numberOnlyPrefereces) {
EditTextPreference editTextPreference = getPreferenceManager().findPreference(key);
- editTextPreference.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
- @Override
- public void onBindEditText(@NonNull EditText editText) {
- editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
- }
- });
+ if (editTextPreference != null) {
+ editTextPreference.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
+ @Override
+ public void onBindEditText(@NonNull EditText editText) {
+ editText.setInputType(InputType.TYPE_CLASS_NUMBER);
+ }
+ });
+ }
}
ListPreference themePreference = (ListPreference) getPreferenceManager().findPreference("theme");
- assert themePreference != null;
- themePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
- @Override
- public boolean onPreferenceChange(Preference preference, Object newValue) {
- if (!keyboardPreferences.getCustomTheme()) {
- int index = Integer.parseInt(newValue.toString());
- preference.setSummary(getResources().getStringArray(R.array.Themes)[index]);
- setThemeByIndex(index);
- return true;
+ if (themePreference != null) {
+ themePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (!keyboardPreferences.getCustomTheme()) {
+ int index = Integer.parseInt(newValue.toString());
+ preference.setSummary(getResources().getStringArray(R.array.Themes)[index]);
+ setThemeByIndex(index);
+ return true;
+ }
+ preference.setSummary("Custom Theme is set");
+ return false;
}
- preference.setSummary("Custom Theme is set");
- return false;
- }
- });
+ });
+ }
Bundle bundle = this.getArguments();
-// Log.d(this.getClass().getSimpleName(), "onCreatePreferences: "+bundle );
- if (bundle != null &&
- (bundle.getInt("notification") == 1)) {
+ if (bundle != null && (bundle.getInt("notification") == 1)) {
scrollToPreference("notification");
}
+ }
+ private void performExport(Uri uri) {
+ try {
+ OutputStream os = requireContext().getContentResolver().openOutputStream(uri);
+ if (os != null) {
+ String error = SettingsManager.exportSettings(requireContext(), os);
+ if (error == null) {
+ Toast.makeText(getActivity(), R.string.export_success, Toast.LENGTH_SHORT).show();
+ } else {
+ String msg = getString(R.string.export_error_format, error);
+ Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ Toast.makeText(getActivity(), R.string.export_failed, Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(getActivity(), "Export error: " + e.getMessage(), Toast.LENGTH_LONG).show();
+ }
}
+ private void validateAndPerformImport(Uri uri) {
+ long fileSize = -1;
+ try (Cursor cursor = requireContext().getContentResolver().query(uri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
+ if (sizeIndex != -1) fileSize = cursor.getLong(sizeIndex);
+ }
+ }
+ // Validate Size (512KB limit)
+ if (fileSize > 512 * 1024) {
+ Toast.makeText(getActivity(), "File too large (max 512KB)", Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ performImport(uri);
+ }
+
+ private void performImport(Uri uri) {
+ try {
+ InputStream is = requireContext().getContentResolver().openInputStream(uri);
+ if (is != null) {
+ String error = SettingsManager.importSettings(requireContext(), is);
+ if (error == null) {
+ Toast.makeText(getActivity(), R.string.import_success, Toast.LENGTH_LONG).show();
+ requireActivity().recreate();
+ } else {
+ String msg = getString(R.string.import_error_format, error);
+ Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show();
+ }
+ } else {
+ Toast.makeText(getActivity(), R.string.import_failed, Toast.LENGTH_LONG).show();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ Toast.makeText(getActivity(), "Import error: " + e.getMessage(), Toast.LENGTH_LONG).show();
+ }
+ }
public static CharSequence getCurrentImeLabel(Context context) {
CharSequence readableName = null;
@@ -113,9 +193,7 @@ public static CharSequence getCurrentImeLabel(Context context) {
@Override
public boolean onPreferenceTreeClick(Preference preference) {
-
if (preference == null || preference.getKey() == null) {
- //Run Intent
return false;
}
switch (preference.getKey()) {
@@ -128,7 +206,8 @@ public boolean onPreferenceTreeClick(Preference preference) {
case "bg_colour_picker":
case "fg_colour_picker":
openColourPicker(preference.getKey());
- getPreferenceManager().findPreference("theme").setSummary("Custom Theme is set");
+ Preference themePref = getPreferenceManager().findPreference("theme");
+ if (themePref != null) themePref.setSummary("Custom Theme is set");
break;
case "restore_default":
confirmReset();
@@ -136,6 +215,12 @@ public boolean onPreferenceTreeClick(Preference preference) {
case "restore_old":
classicSymbols();
break;
+ case "export_settings":
+ exportLauncher.launch(getString(R.string.default_export_filename));
+ break;
+ case "import_settings":
+ importLauncher.launch(new String[]{"application/octet-stream", "application/x-codeboard"});
+ break;
default:
break;
}
@@ -154,11 +239,7 @@ public void onClick(DialogInterface dialogInterface, int i) {
addPreferencesFromResource(R.xml.preferences);
}
})
- .setNegativeButton("No", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- }
- })
+ .setNegativeButton("No", null)
.show();
}
@@ -183,38 +264,20 @@ public void onClick(DialogInterface dialogInterface, int i) {
addPreferencesFromResource(R.xml.preferences);
}
})
- .setNegativeButton("No", new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- }
- })
+ .setNegativeButton("No", null)
.show();
}
private void setThemeByIndex(int index) {
ThemeInfo themeInfo;
switch (index) {
- case 1:
- themeInfo = ThemeDefinitions.MaterialDark();
- break;
- case 2:
- themeInfo = ThemeDefinitions.MaterialWhite();
- break;
- case 3:
- themeInfo = ThemeDefinitions.PureBlack();
- break;
- case 4:
- themeInfo = ThemeDefinitions.White();
- break;
- case 5:
- themeInfo = ThemeDefinitions.Blue();
- break;
- case 6:
- themeInfo = ThemeDefinitions.Purple();
- break;
- default:
- themeInfo = ThemeDefinitions.Default();
- break;
+ case 1: themeInfo = ThemeDefinitions.MaterialDark(); break;
+ case 2: themeInfo = ThemeDefinitions.MaterialWhite(); break;
+ case 3: themeInfo = ThemeDefinitions.PureBlack(); break;
+ case 4: themeInfo = ThemeDefinitions.White(); break;
+ case 5: themeInfo = ThemeDefinitions.Blue(); break;
+ case 6: themeInfo = ThemeDefinitions.Purple(); break;
+ default: themeInfo = ThemeDefinitions.Default(); break;
}
keyboardPreferences.setBgColor(String.valueOf(themeInfo.backgroundColor));
keyboardPreferences.setFgColor(String.valueOf(themeInfo.foregroundColor));
@@ -245,12 +308,13 @@ public void onColorChosen(@ColorInt int color) {
});
}
-
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (hasFocus) {
- Preference imePreference = (Preference) getPreferenceManager().findPreference("change_keyboard");
- imePreference.setSummary(getCurrentImeLabel(getActivity().getApplicationContext()));
+ Preference imePreference = getPreferenceManager().findPreference("change_keyboard");
+ if (imePreference != null) {
+ imePreference.setSummary(getCurrentImeLabel(getActivity().getApplicationContext()));
+ }
}
}
}
diff --git a/app/src/main/kotlin/com/gazlaws/codeboard/MigrationVerification.kt b/app/src/main/kotlin/com/gazlaws/codeboard/MigrationVerification.kt
new file mode 100644
index 00000000..28918ac4
--- /dev/null
+++ b/app/src/main/kotlin/com/gazlaws/codeboard/MigrationVerification.kt
@@ -0,0 +1,13 @@
+package com.gazlaws.codeboard
+
+/**
+ * Dummy Kotlin class to verify that the project successfully compiles Kotlin code
+ * as part of the modernization migration.
+ */
+class MigrationVerification {
+ fun checkInteroperability() {
+ // Just referencing the class is enough for compilation check
+ val className = MainActivity::class.java.simpleName
+ println("Successfully verified interoperability with Java class: $className")
+ }
+}
diff --git a/app/src/main/kotlin/com/gazlaws/codeboard/SettingsManager.kt b/app/src/main/kotlin/com/gazlaws/codeboard/SettingsManager.kt
new file mode 100644
index 00000000..3ec219a5
--- /dev/null
+++ b/app/src/main/kotlin/com/gazlaws/codeboard/SettingsManager.kt
@@ -0,0 +1,91 @@
+package com.gazlaws.codeboard
+
+import android.content.Context
+import androidx.preference.PreferenceManager
+import org.json.JSONObject
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.ByteArrayOutputStream
+
+object SettingsManager {
+
+ private const val MAX_FILE_SIZE = 512 * 1024 // 512KB
+
+ @JvmStatic
+ fun exportSettings(context: Context, outputStream: OutputStream): String? {
+ return try {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ val allPrefs = prefs.all
+ val jsonObject = JSONObject()
+
+ for ((key, value) in allPrefs) {
+ jsonObject.put(key, value)
+ }
+
+ outputStream.bufferedWriter().use { writer ->
+ writer.write(jsonObject.toString())
+ }
+ null
+ } catch (t: Throwable) {
+ t.localizedMessage ?: "Unknown export error"
+ } finally {
+ try { outputStream.close() } catch (ignored: Exception) {}
+ }
+ }
+
+ @JvmStatic
+ fun importSettings(context: Context, inputStream: InputStream): String? {
+ return try {
+ val buffer = ByteArray(60 * 1024) // 60KB
+ val output = ByteArrayOutputStream()
+ var totalBytes = 0
+ var bytesRead: Int
+
+ while (inputStream.read(buffer).also { bytesRead = it } != -1) {
+ totalBytes += bytesRead
+ if (totalBytes > MAX_FILE_SIZE) {
+ return "File too large (max 512KB)"
+ }
+ output.write(buffer, 0, bytesRead)
+ }
+
+ val content = output.toString("UTF-8")
+ if (content.isBlank()) return "File is empty"
+
+ val jsonObject = try {
+ JSONObject(content)
+ } catch (e: Exception) {
+ return "Invalid JSON format: ${e.localizedMessage}"
+ }
+
+ if (jsonObject.length() == 0) {
+ return "This is not a valid settings file (empty)"
+ }
+
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ val editor = prefs.edit()
+
+ val keys = jsonObject.keys()
+ while (keys.hasNext()) {
+ val key = keys.next()
+ if (jsonObject.isNull(key)) continue
+
+ val value = jsonObject.get(key)
+ when (value) {
+ is Boolean -> editor.putBoolean(key, value)
+ is Int -> editor.putInt(key, value)
+ is Long -> editor.putLong(key, value)
+ is Double -> editor.putFloat(key, value.toFloat())
+ is Float -> editor.putFloat(key, value)
+ is String -> editor.putString(key, value)
+ }
+ }
+ editor.apply()
+ null
+ } catch (t: Throwable) {
+ t.localizedMessage ?: "Invalid file or read error"
+ } finally {
+ try { inputStream.close() } catch (ignored: Exception) {}
+ }
+ }
+}
diff --git a/app/src/main/res/values/backup_strings.xml b/app/src/main/res/values/backup_strings.xml
new file mode 100644
index 00000000..0e7784fa
--- /dev/null
+++ b/app/src/main/res/values/backup_strings.xml
@@ -0,0 +1,12 @@
+
+ Export Settings
+ Import Settings
+ Settings exported successfully
+ Export failed
+ Settings imported. Restarting to apply changes...
+ Import failed
+ Backup & Restore
+ settings.codeboard
+ Import failed: %1$s
+ Export failed: %1$s
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 7c744130..0878c3c1 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -188,6 +188,15 @@
android:title="Pin 7:"
app:useSimpleSummaryProvider="true" />
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 0645bbd1..00000000
--- a/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
- repositories {
-// jcenter()
- google()
- mavenCentral()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:8.8.0'
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
-}
-
-allprojects {
- repositories {
-// jcenter()
- google()
- mavenCentral()
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..702920b7
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ id("com.android.application") version "9.2.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.3.10" apply false
+}
+
+tasks.register("clean") {
+ delete(rootProject.buildDir)
+}
diff --git a/gradle.properties b/gradle.properties
index a4d31130..dbd01a84 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,12 +10,12 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
-# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
-android.enableJetifier=true
+android.enableJetifier=false
android.useAndroidX=true
org.gradle.configuration-cache=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index df97d72b..7e7d24f6 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index d3db1092..00000000
--- a/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-include ':app'
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 00000000..616c3db5
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,19 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven { url = uri("https://jitpack.io") }
+ }
+}
+
+rootProject.name = "codeboard"
+include(":app")