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")