diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..eb59bace --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "preferences": { + "git_commit_no_co_author": true + } +} diff --git a/KMP/.gitignore b/KMP/.gitignore new file mode 100644 index 00000000..b1c1e4a4 --- /dev/null +++ b/KMP/.gitignore @@ -0,0 +1,9 @@ +.gradle +build +.kotlin +local.properties +*.podspec +Pods +Podfile.lock +*.xcworkspace +*.xcuserdata diff --git a/KMP/build.gradle.kts b/KMP/build.gradle.kts new file mode 100644 index 00000000..92a528c3 --- /dev/null +++ b/KMP/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + kotlin("multiplatform") version "2.2.20" apply false + kotlin("plugin.compose") version "2.2.20" apply false + kotlin("android") version "2.2.20" apply false + id("com.android.library") version "8.13.1" apply false + id("com.android.application") version "8.13.1" apply false + id("org.jetbrains.compose") version "1.8.1" apply false +} diff --git a/KMP/gradle.properties b/KMP/gradle.properties new file mode 100644 index 00000000..6ef20461 --- /dev/null +++ b/KMP/gradle.properties @@ -0,0 +1,11 @@ +# Project-wide Gradle settings. +org.gradle.jvmargs=-Xmx2048m + +# Android +android.useAndroidX=true +android.nonTransitiveRClass=false + +# Kotlin +kotlin.mpp.stability.nowarn=true +kotlin.native.cacheKind.iosArm64=none +kotlin.native.cacheKind.iosSimulatorArm64=none diff --git a/KMP/gradle/wrapper/gradle-wrapper.jar b/KMP/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..7a3265ee Binary files /dev/null and b/KMP/gradle/wrapper/gradle-wrapper.jar differ diff --git a/KMP/gradle/wrapper/gradle-wrapper.properties b/KMP/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a38550eb --- /dev/null +++ b/KMP/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Oct 16 21:16:39 CST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip diff --git a/KMP/gradlew b/KMP/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/KMP/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/KMP/gradlew.bat b/KMP/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/KMP/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/KMP/mmkv/build.gradle.kts b/KMP/mmkv/build.gradle.kts new file mode 100644 index 00000000..4cce0d5d --- /dev/null +++ b/KMP/mmkv/build.gradle.kts @@ -0,0 +1,91 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + kotlin("multiplatform") + kotlin("native.cocoapods") + id("com.android.library") +} + +kotlin { + // Android target + androidTarget { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) + } + } + + // iOS targets + iosArm64() + iosSimulatorArm64() + iosX64() + + // macOS targets + macosArm64() + macosX64() + + // Use the default hierarchy template which automatically creates + // appleMain, iosMain, macosMain, etc. intermediate source sets. + // We add a custom darwinMain that covers both iOS and macOS. + applyDefaultHierarchyTemplate { + common { + group("darwin") { + group("ios") + group("macos") + } + } + } + + // CocoaPods integration for Darwin platforms + cocoapods { + summary = "MMKV Kotlin Multiplatform wrapper" + homepage = "https://github.com/Tencent/MMKV" + version = "2.4.0" + ios.deploymentTarget = "13.0" + osx.deploymentTarget = "10.15" + + pod("MMKV") { + version = "2.4.0" + } + } + + // Source set hierarchy + sourceSets { + androidMain { + dependencies { + implementation("com.tencent:mmkv:2.4.0") + } + } + } +} + +android { + namespace = "com.tencent.mmkv.kmp" + compileSdk = 35 + + defaultConfig { + minSdk = 23 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} diff --git a/KMP/mmkv/src/androidMain/kotlin/com/tencent/mmkv/kmp/MMKV.android.kt b/KMP/mmkv/src/androidMain/kotlin/com/tencent/mmkv/kmp/MMKV.android.kt new file mode 100644 index 00000000..22f5c1d2 --- /dev/null +++ b/KMP/mmkv/src/androidMain/kotlin/com/tencent/mmkv/kmp/MMKV.android.kt @@ -0,0 +1,281 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mmkv.kmp + +import android.content.Context +import com.tencent.mmkv.MMKV as AndroidMMKV +import com.tencent.mmkv.MMKVConfig as AndroidMMKVConfig +import com.tencent.mmkv.MMKVHandler as AndroidMMKVHandler +import com.tencent.mmkv.MMKVLogLevel as AndroidMMKVLogLevel +import com.tencent.mmkv.MMKVRecoverStrategic as AndroidMMKVRecoverStrategic + +// region Platform-specific initialization (Android needs Context) + +/** + * Initialize MMKV with default configuration. + * You must call one of the initialize() methods on App startup process before using MMKV. + * + * @param context The context of Android App, usually from Application. + * @param logLevel The log level of MMKV, defaults to [MMKVLogLevel.Info]. + * @return The root folder of MMKV, defaults to $(FilesDir)/mmkv. + */ +fun MMKV.Companion.initialize( + context: Context, + logLevel: MMKVLogLevel = MMKVLogLevel.Info, +): String { + return AndroidMMKV.initialize(context, logLevel.toAndroid()) +} + +/** + * Initialize MMKV with customize root folder. + * + * @param context The context of Android App, usually from Application. + * @param rootDir The root folder of MMKV, defaults to $(FilesDir)/mmkv. + * @param logLevel The log level of MMKV, defaults to [MMKVLogLevel.Info]. + * @param handler The unified callback handler for MMKV. + * @return The root folder of MMKV. + */ +fun MMKV.Companion.initialize( + context: Context, + rootDir: String, + logLevel: MMKVLogLevel = MMKVLogLevel.Info, + handler: MMKVHandler? = null, +): String { + val androidHandler = handler?.let { AndroidMMKVHandlerAdapter(it) } + return AndroidMMKV.initialize(context, rootDir, null, logLevel.toAndroid(), androidHandler) +} + +// endregion + +/** + * The Android actual implementation of [MMKV] that delegates to the Java `com.tencent.mmkv.MMKV` library. + * + * Since the KMP class is now in `com.tencent.mmkv.kmp`, we can reference the Java + * `com.tencent.mmkv.MMKV` directly via import alias without any naming collision. + */ +actual class MMKV internal constructor(private val impl: AndroidMMKV) { + + actual companion object { + actual fun onExit() = AndroidMMKV.onExit() + + actual fun setLogLevel(level: MMKVLogLevel) = AndroidMMKV.setLogLevel(level.toAndroid()) + + actual fun defaultMMKV(): MMKV = MMKV(AndroidMMKV.defaultMMKV()) + + actual fun defaultMMKV(config: MMKVConfig): MMKV = MMKV(AndroidMMKV.defaultMMKV(config.toAndroid())) + + actual fun mmkvWithID(mmapID: String): MMKV = MMKV(AndroidMMKV.mmkvWithID(mmapID)) + + actual fun mmkvWithID(mmapID: String, config: MMKVConfig): MMKV = MMKV(AndroidMMKV.mmkvWithID(mmapID, config.toAndroid())) + + actual fun pageSize(): Int = AndroidMMKV.pageSize() + + actual fun version(): String = AndroidMMKV.version() + + actual fun rootDir(): String = AndroidMMKV.getRootDir() ?: "" + + actual fun backupOneToDirectory(mmapID: String, dstDir: String, rootPath: String?): Boolean = + AndroidMMKV.backupOneToDirectory(mmapID, dstDir, rootPath) + + actual fun restoreOneFromDirectory(mmapID: String, srcDir: String, rootPath: String?): Boolean = + AndroidMMKV.restoreOneMMKVFromDirectory(mmapID, srcDir, rootPath) + + actual fun backupAllToDirectory(dstDir: String): Long = AndroidMMKV.backupAllToDirectory(dstDir) + + actual fun restoreAllFromDirectory(srcDir: String): Long = AndroidMMKV.restoreAllFromDirectory(srcDir) + + actual fun isFileValid(mmapID: String, rootPath: String?): Boolean = AndroidMMKV.isFileValid(mmapID, rootPath) + + actual fun removeStorage(mmapID: String, rootPath: String?): Boolean = AndroidMMKV.removeStorage(mmapID, rootPath) + + actual fun checkExist(mmapID: String, rootPath: String?): Boolean = AndroidMMKV.checkExist(mmapID, rootPath) + + actual fun registerHandler(handler: MMKVHandler) = AndroidMMKV.registerHandler(AndroidMMKVHandlerAdapter(handler)) + + actual fun unRegisterHandler() = AndroidMMKV.unregisterHandler() + } + + // region Properties + + actual val mmapID: String get() = impl.mmapID() + actual val isMultiProcess: Boolean get() = impl.isMultiProcess + actual val isReadOnly: Boolean get() = impl.isReadOnly + actual val totalSize: Long get() = impl.totalSize() + actual val actualSize: Long get() = impl.actualSize() + actual val count: Long get() = impl.count() + actual val allKeys: List get() = impl.allKeys()?.toList() ?: emptyList() + actual val cryptKey: String? get() = impl.cryptKey() + + // endregion + + // region Encode + + actual fun encodeBool(key: String, value: Boolean): Boolean = impl.encode(key, value) + actual fun encodeBool(key: String, value: Boolean, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt()) + actual fun encodeInt(key: String, value: Int): Boolean = impl.encode(key, value) + actual fun encodeInt(key: String, value: Int, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt()) + actual fun encodeLong(key: String, value: Long): Boolean = impl.encode(key, value) + actual fun encodeLong(key: String, value: Long, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt()) + actual fun encodeFloat(key: String, value: Float): Boolean = impl.encode(key, value) + actual fun encodeFloat(key: String, value: Float, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt()) + actual fun encodeDouble(key: String, value: Double): Boolean = impl.encode(key, value) + actual fun encodeDouble(key: String, value: Double, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt()) + actual fun encodeString(key: String, value: String): Boolean = impl.encode(key, value) + actual fun encodeString(key: String, value: String, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt()) + actual fun encodeBytes(key: String, value: ByteArray): Boolean = impl.encode(key, value) + actual fun encodeBytes(key: String, value: ByteArray, expireDuration: UInt): Boolean = impl.encode(key, value, expireDuration.toInt()) + + // endregion + + // region Decode + + actual fun decodeBool(key: String, defaultValue: Boolean): Boolean = impl.decodeBool(key, defaultValue) + actual fun decodeInt(key: String, defaultValue: Int): Int = impl.decodeInt(key, defaultValue) + actual fun decodeLong(key: String, defaultValue: Long): Long = impl.decodeLong(key, defaultValue) + actual fun decodeFloat(key: String, defaultValue: Float): Float = impl.decodeFloat(key, defaultValue) + actual fun decodeDouble(key: String, defaultValue: Double): Double = impl.decodeDouble(key, defaultValue) + actual fun decodeString(key: String, defaultValue: String?): String? = impl.decodeString(key, defaultValue) + actual fun decodeBytes(key: String): ByteArray? = impl.decodeBytes(key) + + // endregion + + // region Key management + + actual fun containsKey(key: String): Boolean = impl.containsKey(key) + actual fun countNonExpiredKeys(): Long = impl.countNonExpiredKeys() + actual fun allNonExpiredKeys(): List = impl.allNonExpireKeys()?.toList() ?: emptyList() + actual fun removeValueForKey(key: String) = impl.removeValueForKey(key) + actual fun removeValuesForKeys(keys: List) = impl.removeValuesForKeys(keys.toTypedArray()) + actual fun clearAll() = impl.clearAll() + actual fun clearAllKeepSpace() = impl.clearAllWithKeepingSpace() + + // endregion + + // region Encryption + + actual fun reKey(newKey: String?, aes256: Boolean): Boolean = impl.reKey(newKey, aes256) + actual fun checkReSetCryptKey(cryptKey: String?, aes256: Boolean) = impl.checkReSetCryptKey(cryptKey, aes256) + + // endregion + + // region Utility + + actual fun sync() = impl.sync() + actual fun async() = impl.async() + actual fun trim() = impl.trim() + actual fun close() = impl.close() + actual fun clearMemoryCache() = impl.clearMemoryCache() + + actual fun importFrom(source: MMKV): Long = impl.importFrom(source.impl) + + actual fun enableAutoKeyExpire(expiredInSeconds: UInt): Boolean = impl.enableAutoKeyExpire(expiredInSeconds.toInt()) + actual fun disableAutoKeyExpire(): Boolean = impl.disableAutoKeyExpire() + + actual fun enableCompareBeforeSet(): Boolean { + impl.enableCompareBeforeSet() + return true + } + + actual fun disableCompareBeforeSet(): Boolean { + impl.disableCompareBeforeSet() + return true + } + + actual fun checkContentChanged() = impl.checkContentChangedByOuterProcess() + + // endregion +} + +// region Enum conversion helpers + +private fun MMKVLogLevel.toAndroid(): AndroidMMKVLogLevel = when (this) { + MMKVLogLevel.Debug -> AndroidMMKVLogLevel.LevelDebug + MMKVLogLevel.Info -> AndroidMMKVLogLevel.LevelInfo + MMKVLogLevel.Warning -> AndroidMMKVLogLevel.LevelWarning + MMKVLogLevel.Error -> AndroidMMKVLogLevel.LevelError + MMKVLogLevel.None -> AndroidMMKVLogLevel.LevelNone +} + +private fun AndroidMMKVLogLevel.toKmp(): MMKVLogLevel = when (this) { + AndroidMMKVLogLevel.LevelDebug -> MMKVLogLevel.Debug + AndroidMMKVLogLevel.LevelInfo -> MMKVLogLevel.Info + AndroidMMKVLogLevel.LevelWarning -> MMKVLogLevel.Warning + AndroidMMKVLogLevel.LevelError -> MMKVLogLevel.Error + AndroidMMKVLogLevel.LevelNone -> MMKVLogLevel.None +} + +private fun MMKVRecoverStrategic.toAndroid(): AndroidMMKVRecoverStrategic = when (this) { + MMKVRecoverStrategic.OnErrorDiscard -> AndroidMMKVRecoverStrategic.OnErrorDiscard + MMKVRecoverStrategic.OnErrorRecover -> AndroidMMKVRecoverStrategic.OnErrorRecover +} + +// endregion + +// region Config conversion + +private fun MMKVConfig.toAndroid(): AndroidMMKVConfig { + val config = AndroidMMKVConfig() + config.mode = mode + config.cryptKey = cryptKey + config.aes256 = aes256 + config.rootPath = rootPath + config.expectedCapacity = expectedCapacity + config.enableKeyExpire = enableKeyExpire + config.expiredInSeconds = expiredInSeconds.toInt() + config.enableCompareBeforeSet = enableCompareBeforeSet + config.recover = recover?.toAndroid() + config.itemSizeLimit = itemSizeLimit.toInt() + return config +} + +// endregion + +// region Handler adapter + +/** + * Adapts the KMP [MMKVHandler] to the Java [AndroidMMKVHandler] interface. + */ +private class AndroidMMKVHandlerAdapter( + private val handler: MMKVHandler, +) : AndroidMMKVHandler { + + override fun onMMKVCRCCheckFail(mmapID: String): AndroidMMKVRecoverStrategic { + return handler.onMMKVCRCCheckFail(mmapID).toAndroid() + } + + override fun onMMKVFileLengthError(mmapID: String): AndroidMMKVRecoverStrategic { + return handler.onMMKVFileLengthError(mmapID).toAndroid() + } + + override fun wantLogRedirecting(): Boolean = handler.wantLogRedirect() + + override fun mmkvLog(level: AndroidMMKVLogLevel, file: String, line: Int, function: String, message: String) { + handler.mmkvLog(level.toKmp(), file, line, function, message) + } + + override fun wantContentChangeNotification(): Boolean = handler.wantContentChangeNotification() + + override fun onContentChangedByOuterProcess(mmapID: String) = handler.onContentChangedByOuterProcess(mmapID) + + override fun onMMKVContentLoadSuccessfully(mmapID: String) = handler.onMMKVContentLoadSuccessfully(mmapID) +} + +// endregion diff --git a/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKV.kt b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKV.kt new file mode 100644 index 00000000..14ece500 --- /dev/null +++ b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKV.kt @@ -0,0 +1,321 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mmkv.kmp + +/** + * An highly efficient, reliable, multi-process key-value storage framework. + * Kotlin Multiplatform wrapper for MMKV. + * + * Note: Platform-specific initialization methods are provided as extension functions + * on [MMKV.Companion] in each platform source set, since Android requires [Context] + * while other platforms do not. + */ +expect class MMKV { + + companion object { + /** + * Notify MMKV that App is about to exit. It's totally fine not calling it at all. + */ + fun onExit() + + /** + * Set the log level of MMKV. + * @param level Defaults to [MMKVLogLevel.Info]. + */ + fun setLogLevel(level: MMKVLogLevel) + + /** + * Create the default MMKV instance in single-process mode. + */ + fun defaultMMKV(): MMKV + + /** + * Create the default MMKV instance with the given configuration. + * @param config The all-in-one configuration for the MMKV instance. + */ + fun defaultMMKV(config: MMKVConfig): MMKV + + /** + * Create an MMKV instance with an unique ID (in single-process mode). + * @param mmapID The unique ID of the MMKV instance. + */ + fun mmkvWithID(mmapID: String): MMKV + + /** + * Create an MMKV instance with an unique ID and the given configuration. + * @param mmapID The unique ID of the MMKV instance. + * @param config The all-in-one configuration for the MMKV instance. + */ + fun mmkvWithID(mmapID: String, config: MMKVConfig): MMKV + + /** + * @return The device's memory page size. + */ + fun pageSize(): Int + + /** + * @return The version of MMKV. + */ + fun version(): String + + /** + * @return The root folder of MMKV. + */ + fun rootDir(): String + + /** + * Backup one MMKV instance to dstDir. + * @param mmapID The MMKV ID to backup. + * @param dstDir The backup destination directory. + * @param rootPath The customize root path of the MMKV, if null then backup from the root dir of MMKV. + */ + fun backupOneToDirectory(mmapID: String, dstDir: String, rootPath: String? = null): Boolean + + /** + * Restore one MMKV instance from srcDir. + * @param mmapID The MMKV ID to restore. + * @param srcDir The restore source directory. + * @param rootPath The customize root path of the MMKV, if null then restore to the root dir of MMKV. + */ + fun restoreOneFromDirectory(mmapID: String, srcDir: String, rootPath: String? = null): Boolean + + /** + * Backup all MMKV instances to dstDir. + * @param dstDir The backup destination directory. + * @return Count of MMKV successfully backed up. + */ + fun backupAllToDirectory(dstDir: String): Long + + /** + * Restore all MMKV instances from srcDir. + * @param srcDir The restore source directory. + * @return Count of MMKV successfully restored. + */ + fun restoreAllFromDirectory(srcDir: String): Long + + /** + * Check whether the MMKV file is valid or not. + * Note: Don't use this to check the existence of the instance, the result is undefined on nonexistent files. + */ + fun isFileValid(mmapID: String, rootPath: String? = null): Boolean + + /** + * Remove the storage of the MMKV, including the data file & meta file (.crc). + * Note: the existing instance (if any) will be closed & destroyed. + */ + fun removeStorage(mmapID: String, rootPath: String? = null): Boolean + + /** + * Check existence of the MMKV file. + */ + fun checkExist(mmapID: String, rootPath: String? = null): Boolean + + /** + * Register a unified callback handler for MMKV. + */ + fun registerHandler(handler: MMKVHandler) + + /** + * Unregister the callback handler. + */ + fun unRegisterHandler() + } + + /** The unique ID of the MMKV instance. */ + val mmapID: String + + /** Whether the MMKV instance is in multi-process mode. */ + val isMultiProcess: Boolean + + /** Whether the MMKV instance is in read-only mode. */ + val isReadOnly: Boolean + + /** + * Get the size of the underlying file. + * Align to the disk block size, typically 4K for an Android device. + */ + val totalSize: Long + + /** + * Get the actual used size of the MMKV instance. + */ + val actualSize: Long + + /** + * The total count of all the keys. + */ + val count: Long + + /** + * All keys. + */ + val allKeys: List + + /** + * The encryption key (no more than 16 bytes, or 32 bytes for AES-256). + */ + val cryptKey: String? + + // region Encode + + fun encodeBool(key: String, value: Boolean): Boolean + fun encodeBool(key: String, value: Boolean, expireDuration: UInt): Boolean + fun encodeInt(key: String, value: Int): Boolean + fun encodeInt(key: String, value: Int, expireDuration: UInt): Boolean + fun encodeLong(key: String, value: Long): Boolean + fun encodeLong(key: String, value: Long, expireDuration: UInt): Boolean + fun encodeFloat(key: String, value: Float): Boolean + fun encodeFloat(key: String, value: Float, expireDuration: UInt): Boolean + fun encodeDouble(key: String, value: Double): Boolean + fun encodeDouble(key: String, value: Double, expireDuration: UInt): Boolean + fun encodeString(key: String, value: String): Boolean + fun encodeString(key: String, value: String, expireDuration: UInt): Boolean + fun encodeBytes(key: String, value: ByteArray): Boolean + fun encodeBytes(key: String, value: ByteArray, expireDuration: UInt): Boolean + + // endregion + + // region Decode + + fun decodeBool(key: String, defaultValue: Boolean = false): Boolean + fun decodeInt(key: String, defaultValue: Int = 0): Int + fun decodeLong(key: String, defaultValue: Long = 0L): Long + fun decodeFloat(key: String, defaultValue: Float = 0f): Float + fun decodeDouble(key: String, defaultValue: Double = 0.0): Double + fun decodeString(key: String, defaultValue: String? = null): String? + fun decodeBytes(key: String): ByteArray? + + // endregion + + // region Key management + + /** + * Check whether or not MMKV contains the key. + */ + fun containsKey(key: String): Boolean + + /** + * @return Count of non-expired keys. Note that this call has costs. + */ + fun countNonExpiredKeys(): Long + + /** + * @return All non-expired keys. Note that this call has costs. + */ + fun allNonExpiredKeys(): List + + fun removeValueForKey(key: String) + fun removeValuesForKeys(keys: List) + + /** + * Clear all the key-values inside the MMKV instance. + */ + fun clearAll() + + /** + * Faster [clearAll] implementation. The file size is kept as previous for later use. + */ + fun clearAllKeepSpace() + + // endregion + + // region Encryption + + /** + * Transform plain text into encrypted text, or vice versa by passing a null encryption key. + * You can also change existing crypt key with a different cryptKey. + * @param newKey The new encryption key, or null to remove encryption. + * @param aes256 Use AES 256 key length. + */ + fun reKey(newKey: String?, aes256: Boolean = false): Boolean + + /** + * Just reset the encryption key (will not encrypt or decrypt anything). + * Usually you should call this method after another process has [reKey] the multi-process MMKV instance. + */ + fun checkReSetCryptKey(cryptKey: String?, aes256: Boolean = false) + + // endregion + + // region Utility + + /** + * Save all mmap memory to file synchronously. + */ + fun sync() + + /** + * Save all mmap memory to file asynchronously. + */ + fun async() + + /** + * The [totalSize] of an MMKV instance won't reduce after deleting key-values, + * call this method after lots of deleting if you care about disk usage. + */ + fun trim() + + /** + * Call this method if the MMKV instance is no longer needed in the near future. + * Any subsequent call to the instance is undefined behavior. + */ + fun close() + + /** + * Clear memory cache of the MMKV instance. + * You can call it on memory warning. + */ + fun clearMemoryCache() + + /** + * Import all key-value items from source. + * @return Count of items imported. + */ + fun importFrom(source: MMKV): Long + + /** + * Enable auto key expiration. + * @param expiredInSeconds 0 means no common expiration duration, aka each key will have its own expire date. + */ + fun enableAutoKeyExpire(expiredInSeconds: UInt = 0u): Boolean + + /** + * Disable auto key expiration. + */ + fun disableAutoKeyExpire(): Boolean + + /** + * Enable data compare before set, for better performance. + */ + fun enableCompareBeforeSet(): Boolean + + /** + * Disable data compare before set. + */ + fun disableCompareBeforeSet(): Boolean + + /** + * Check if content changed by other process. + */ + fun checkContentChanged() + + // endregion +} diff --git a/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVConfig.kt b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVConfig.kt new file mode 100644 index 00000000..c24cee10 --- /dev/null +++ b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVConfig.kt @@ -0,0 +1,37 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mmkv.kmp + +/** + * The all-in-one configuration for creating an MMKV instance. + */ +data class MMKVConfig( + val mode: Int = MMKVMode.SINGLE_PROCESS, + val cryptKey: String? = null, + val aes256: Boolean = false, + val rootPath: String? = null, + val expectedCapacity: Long = 0, + val enableKeyExpire: Boolean? = null, + val expiredInSeconds: UInt = 0u, + val enableCompareBeforeSet: Boolean = false, + val recover: MMKVRecoverStrategic? = null, + val itemSizeLimit: UInt = 0u, +) diff --git a/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVHandler.kt b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVHandler.kt new file mode 100644 index 00000000..e6d1ccc5 --- /dev/null +++ b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVHandler.kt @@ -0,0 +1,74 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mmkv.kmp + +/** + * Unified callback handler for MMKV. + * Callback is called on the operating thread of the MMKV instance. + */ +abstract class MMKVHandler { + + /** + * By default MMKV will discard all data on crc32-check failure. + * @param mmapID The unique ID of the MMKV instance. + * @return Return [MMKVRecoverStrategic.OnErrorRecover] to recover any data on the file. + */ + open fun onMMKVCRCCheckFail(mmapID: String): MMKVRecoverStrategic = MMKVRecoverStrategic.OnErrorDiscard + + /** + * By default MMKV will discard all data on file length mismatch. + * @param mmapID The unique ID of the MMKV instance. + * @return Return [MMKVRecoverStrategic.OnErrorRecover] to recover any data on the file. + */ + open fun onMMKVFileLengthError(mmapID: String): MMKVRecoverStrategic = MMKVRecoverStrategic.OnErrorDiscard + + /** + * @return Return true if you want log redirecting. + */ + open fun wantLogRedirect(): Boolean = false + + /** + * Log redirecting. + * @param level The level of this log. + * @param file The file name of this log. + * @param line The line of code of this log. + * @param function The function name of this log. + * @param message The content of this log. + */ + open fun mmkvLog(level: MMKVLogLevel, file: String, line: Int, function: String, message: String) {} + + /** + * @return Return true if you want inter-process content change notification. + */ + open fun wantContentChangeNotification(): Boolean = false + + /** + * Inter-process content change notification. + * @param mmapID The unique ID of the changed MMKV instance. + */ + open fun onContentChangedByOuterProcess(mmapID: String) {} + + /** + * Called when an MMKV file is loaded successfully. + * @param mmapID The unique ID of the loaded MMKV instance. + */ + open fun onMMKVContentLoadSuccessfully(mmapID: String) {} +} diff --git a/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVLogLevel.kt b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVLogLevel.kt new file mode 100644 index 00000000..9459f0b6 --- /dev/null +++ b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVLogLevel.kt @@ -0,0 +1,41 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mmkv.kmp + +/** + * The levels of MMKV log. + */ +enum class MMKVLogLevel { + /** Debug level. Not available for release/production build. */ + Debug, + + /** Info level. The default level. */ + Info, + + /** Warning level. */ + Warning, + + /** Error level. */ + Error, + + /** Special level for disabling all logging. */ + None +} diff --git a/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVMode.kt b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVMode.kt new file mode 100644 index 00000000..cd2b19af --- /dev/null +++ b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVMode.kt @@ -0,0 +1,47 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mmkv.kmp + +/** + * MMKV instance mode constants. + */ +object MMKVMode { + /** Single-process mode. The default mode on an MMKV instance. */ + const val SINGLE_PROCESS = 1 shl 0 + + /** Multi-process mode. */ + const val MULTI_PROCESS = 1 shl 1 + + /** Read-only mode. */ + const val READ_ONLY = 1 shl 5 +} + +/** + * MMKV expire duration constants (in seconds). + */ +object MMKVExpireDuration { + const val Never: UInt = 0u + const val InMinute: UInt = 60u + const val InHour: UInt = 3_600u + const val InDay: UInt = 86_400u + const val InMonth: UInt = 2_592_000u + const val InYear: UInt = 946_080_000u +} diff --git a/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVRecoverStrategic.kt b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVRecoverStrategic.kt new file mode 100644 index 00000000..b2e4c681 --- /dev/null +++ b/KMP/mmkv/src/commonMain/kotlin/com/tencent/mmkv/kmp/MMKVRecoverStrategic.kt @@ -0,0 +1,32 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mmkv.kmp + +/** + * The recover strategic of MMKV on errors. + */ +enum class MMKVRecoverStrategic { + /** The default strategic is to discard everything on errors. */ + OnErrorDiscard, + + /** The recover strategic will try to recover as much data as possible. */ + OnErrorRecover +} diff --git a/KMP/mmkv/src/darwinMain/kotlin/com/tencent/mmkv/kmp/MMKV.darwin.kt b/KMP/mmkv/src/darwinMain/kotlin/com/tencent/mmkv/kmp/MMKV.darwin.kt new file mode 100644 index 00000000..b86a74df --- /dev/null +++ b/KMP/mmkv/src/darwinMain/kotlin/com/tencent/mmkv/kmp/MMKV.darwin.kt @@ -0,0 +1,419 @@ +/* + * Tencent is pleased to support the open source community by making + * MMKV available. + * + * Copyright (C) 2026 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of + * the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mmkv.kmp + +import cocoapods.MMKV.MMKV as DarwinMMKV +import cocoapods.MMKV.MMKVConfig as DarwinMMKVConfig +import cocoapods.MMKV.MMKVHandlerProtocol +import cocoapods.MMKV.MMKVLogDebug +import cocoapods.MMKV.MMKVLogError +import cocoapods.MMKV.MMKVLogInfo +import cocoapods.MMKV.MMKVLogLevel as DarwinMMKVLogLevel +import cocoapods.MMKV.MMKVLogNone +import cocoapods.MMKV.MMKVLogWarning +import cocoapods.MMKV.MMKVMultiProcess +import cocoapods.MMKV.MMKVOnErrorDiscard +import cocoapods.MMKV.MMKVOnErrorNotSet +import cocoapods.MMKV.MMKVOnErrorRecover +import cocoapods.MMKV.MMKVReadOnly +import cocoapods.MMKV.MMKVRecoverStrategic as DarwinMMKVRecoverStrategic +import cocoapods.MMKV.MMKVSingleProcess +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.cValue +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.toKString +import kotlinx.cinterop.usePinned +import platform.Foundation.NSData +import platform.Foundation.NSNumber +import platform.Foundation.NSString +import platform.Foundation.create +import platform.Foundation.dataUsingEncoding +import platform.Foundation.NSUTF8StringEncoding +import platform.darwin.NSObject +import platform.posix.memcpy + +// region Platform-specific initialization (Darwin doesn't need Context) + +/** + * Initialize MMKV with customize settings. + * Call this in main thread, before calling any other MMKV methods. + * + * @param rootDir The root dir of MMKV, passing null defaults to {NSDocumentDirectory}/mmkv. + * @param logLevel MMKVLogInfo by default, MMKVLogNone to disable all logging. + * @param handler The unified callback handler for MMKV. + * @return Root dir of MMKV. + */ +@OptIn(ExperimentalForeignApi::class) +fun MMKV.Companion.initialize( + rootDir: String? = null, + logLevel: MMKVLogLevel = MMKVLogLevel.Info, + handler: MMKVHandler? = null, +): String { + val darwinHandler = handler?.let { DarwinMMKVHandlerAdapter(it) } + return DarwinMMKV.initializeMMKV(rootDir, logLevel = logLevel.toDarwin(), handler = darwinHandler) ?: "" +} + +/** + * Initialize MMKV with a group directory for multi-process access. + * + * @param rootDir The root dir of MMKV, passing null defaults to {NSDocumentDirectory}/mmkv. + * @param groupDir The root dir of multi-process MMKV. + * @param logLevel MMKVLogInfo by default. + * @param handler The unified callback handler for MMKV. + * @return Root dir of MMKV. + */ +@OptIn(ExperimentalForeignApi::class) +fun MMKV.Companion.initialize( + rootDir: String? = null, + groupDir: String, + logLevel: MMKVLogLevel = MMKVLogLevel.Info, + handler: MMKVHandler? = null, +): String { + val darwinHandler = handler?.let { DarwinMMKVHandlerAdapter(it) } + return DarwinMMKV.initializeMMKV(rootDir, groupDir = groupDir, logLevel = logLevel.toDarwin(), handler = darwinHandler) ?: "" +} + +// endregion + +@OptIn(ExperimentalForeignApi::class) +actual class MMKV internal constructor(private val impl: DarwinMMKV) { + + actual companion object { + actual fun onExit() { + DarwinMMKV.onAppTerminate() + } + + actual fun setLogLevel(level: MMKVLogLevel) { + @Suppress("DEPRECATION") + DarwinMMKV.setLogLevel(level.toDarwin()) + } + + actual fun defaultMMKV(): MMKV { + return MMKV(DarwinMMKV.defaultMMKV()!!) + } + + actual fun defaultMMKV(config: MMKVConfig): MMKV { + return MMKV(DarwinMMKV.defaultMMKVWithConfig(config.toDarwinCValue())!!) + } + + actual fun mmkvWithID(mmapID: String): MMKV { + return MMKV(DarwinMMKV.mmkvWithID(mmapID)!!) + } + + actual fun mmkvWithID(mmapID: String, config: MMKVConfig): MMKV { + return MMKV(DarwinMMKV.mmkvWithID(mmapID, config = config.toDarwinCValue())!!) + } + + actual fun pageSize(): Int { + return DarwinMMKV.pageSize().toInt() + } + + actual fun version(): String { + return DarwinMMKV.version() + } + + actual fun rootDir(): String { + return DarwinMMKV.mmkvBasePath() + } + + actual fun backupOneToDirectory(mmapID: String, dstDir: String, rootPath: String?): Boolean { + return DarwinMMKV.backupOneMMKV(mmapID, rootPath = rootPath, toDirectory = dstDir) + } + + actual fun restoreOneFromDirectory(mmapID: String, srcDir: String, rootPath: String?): Boolean { + return DarwinMMKV.restoreOneMMKV(mmapID, rootPath = rootPath, fromDirectory = srcDir) + } + + actual fun backupAllToDirectory(dstDir: String): Long { + return DarwinMMKV.backupAll(null, toDirectory = dstDir).toLong() + } + + actual fun restoreAllFromDirectory(srcDir: String): Long { + return DarwinMMKV.restoreAll(null, fromDirectory = srcDir).toLong() + } + + actual fun isFileValid(mmapID: String, rootPath: String?): Boolean { + return DarwinMMKV.isFileValid(mmapID, rootPath = rootPath) + } + + actual fun removeStorage(mmapID: String, rootPath: String?): Boolean { + return DarwinMMKV.removeStorage(mmapID, rootPath = rootPath) + } + + actual fun checkExist(mmapID: String, rootPath: String?): Boolean { + return DarwinMMKV.checkExist(mmapID, rootPath = rootPath) + } + + private var currentHandler: DarwinMMKVHandlerAdapter? = null + + actual fun registerHandler(handler: MMKVHandler) { + val adapter = DarwinMMKVHandlerAdapter(handler) + currentHandler = adapter + @Suppress("DEPRECATION") + DarwinMMKV.registerHandler(adapter) + } + + actual fun unRegisterHandler() { + currentHandler = null + DarwinMMKV.unregiserHandler() + } + } + + // region Properties + + actual val mmapID: String get() = impl.mmapID() + + actual val isMultiProcess: Boolean get() = impl.isMultiProcess() + + actual val isReadOnly: Boolean get() = impl.isReadOnly() + + actual val totalSize: Long get() = impl.totalSize().toLong() + + actual val actualSize: Long get() = impl.actualSize().toLong() + + actual val count: Long get() = impl.count().toLong() + + @Suppress("UNCHECKED_CAST") + actual val allKeys: List get() = (impl.allKeys() as? List) ?: emptyList() + + actual val cryptKey: String? + get() { + val data = impl.cryptKey() ?: return null + return NSString.create(data = data, encoding = NSUTF8StringEncoding) as? String + } + + // endregion + + // region Encode + + actual fun encodeBool(key: String, value: Boolean): Boolean = impl.setBool(value, forKey = key) + actual fun encodeBool(key: String, value: Boolean, expireDuration: UInt): Boolean = impl.setBool(value, forKey = key, expireDuration = expireDuration) + actual fun encodeInt(key: String, value: Int): Boolean = impl.setInt32(value, forKey = key) + actual fun encodeInt(key: String, value: Int, expireDuration: UInt): Boolean = impl.setInt32(value, forKey = key, expireDuration = expireDuration) + actual fun encodeLong(key: String, value: Long): Boolean = impl.setInt64(value, forKey = key) + actual fun encodeLong(key: String, value: Long, expireDuration: UInt): Boolean = impl.setInt64(value, forKey = key, expireDuration = expireDuration) + actual fun encodeFloat(key: String, value: Float): Boolean = impl.setFloat(value, forKey = key) + actual fun encodeFloat(key: String, value: Float, expireDuration: UInt): Boolean = impl.setFloat(value, forKey = key, expireDuration = expireDuration) + actual fun encodeDouble(key: String, value: Double): Boolean = impl.setDouble(value, forKey = key) + actual fun encodeDouble(key: String, value: Double, expireDuration: UInt): Boolean = impl.setDouble(value, forKey = key, expireDuration = expireDuration) + actual fun encodeString(key: String, value: String): Boolean = impl.setString(value, forKey = key) + actual fun encodeString(key: String, value: String, expireDuration: UInt): Boolean = impl.setString(value, forKey = key, expireDuration = expireDuration) + + actual fun encodeBytes(key: String, value: ByteArray): Boolean { + val data = value.toNSData() + return impl.setData(data, forKey = key) + } + + actual fun encodeBytes(key: String, value: ByteArray, expireDuration: UInt): Boolean { + val data = value.toNSData() + return impl.setData(data, forKey = key, expireDuration = expireDuration) + } + + // endregion + + // region Decode + + actual fun decodeBool(key: String, defaultValue: Boolean): Boolean = impl.getBoolForKey(key, defaultValue = defaultValue) + actual fun decodeInt(key: String, defaultValue: Int): Int = impl.getInt32ForKey(key, defaultValue = defaultValue) + actual fun decodeLong(key: String, defaultValue: Long): Long = impl.getInt64ForKey(key, defaultValue = defaultValue) + actual fun decodeFloat(key: String, defaultValue: Float): Float = impl.getFloatForKey(key, defaultValue = defaultValue) + actual fun decodeDouble(key: String, defaultValue: Double): Double = impl.getDoubleForKey(key, defaultValue = defaultValue) + actual fun decodeString(key: String, defaultValue: String?): String? = impl.getStringForKey(key, defaultValue = defaultValue) + + actual fun decodeBytes(key: String): ByteArray? { + val data = impl.getDataForKey(key) ?: return null + return data.toByteArray() + } + + // endregion + + // region Key management + + actual fun containsKey(key: String): Boolean = impl.containsKey(key) + + actual fun countNonExpiredKeys(): Long = impl.countNonExpiredKeys().toLong() + + @Suppress("UNCHECKED_CAST") + actual fun allNonExpiredKeys(): List = (impl.allNonExpiredKeys() as? List) ?: emptyList() + + actual fun removeValueForKey(key: String) = impl.removeValueForKey(key) + + actual fun removeValuesForKeys(keys: List) = impl.removeValuesForKeys(keys) + + actual fun clearAll() = impl.clearAll() + + actual fun clearAllKeepSpace() = impl.clearAllWithKeepingSpace() + + // endregion + + // region Encryption + + actual fun reKey(newKey: String?, aes256: Boolean): Boolean { + val keyData = newKey?.toNSData() + return impl.reKey(keyData, aes256 = aes256) + } + + actual fun checkReSetCryptKey(cryptKey: String?, aes256: Boolean) { + val keyData = cryptKey?.toNSData() + impl.checkReSetCryptKey(keyData, aes256 = aes256) + } + + // endregion + + // region Utility + + actual fun sync() = impl.sync() + actual fun async() = impl.async() + actual fun trim() = impl.trim() + actual fun close() = impl.close() + actual fun clearMemoryCache() = impl.clearMemoryCache() + + actual fun importFrom(source: MMKV): Long = impl.importFrom(source.impl).toLong() + + actual fun enableAutoKeyExpire(expiredInSeconds: UInt): Boolean = impl.enableAutoKeyExpire(expiredInSeconds) + actual fun disableAutoKeyExpire(): Boolean = impl.disableAutoKeyExpire() + + actual fun enableCompareBeforeSet(): Boolean = impl.enableCompareBeforeSet() + actual fun disableCompareBeforeSet(): Boolean = impl.disableCompareBeforeSet() + + actual fun checkContentChanged() = impl.checkContentChanged() + + // endregion +} + +// region Conversion helpers + +@OptIn(ExperimentalForeignApi::class) +private fun MMKVLogLevel.toDarwin(): DarwinMMKVLogLevel = when (this) { + MMKVLogLevel.Debug -> MMKVLogDebug + MMKVLogLevel.Info -> MMKVLogInfo + MMKVLogLevel.Warning -> MMKVLogWarning + MMKVLogLevel.Error -> MMKVLogError + MMKVLogLevel.None -> MMKVLogNone +} + +@OptIn(ExperimentalForeignApi::class) +private fun darwinLogLevelToCommon(level: DarwinMMKVLogLevel): MMKVLogLevel = when (level) { + MMKVLogDebug -> MMKVLogLevel.Debug + MMKVLogInfo -> MMKVLogLevel.Info + MMKVLogWarning -> MMKVLogLevel.Warning + MMKVLogError -> MMKVLogLevel.Error + MMKVLogNone -> MMKVLogLevel.None + else -> MMKVLogLevel.Info +} + +@OptIn(ExperimentalForeignApi::class) +private fun MMKVRecoverStrategic.toDarwin(): DarwinMMKVRecoverStrategic = when (this) { + MMKVRecoverStrategic.OnErrorDiscard -> MMKVOnErrorDiscard + MMKVRecoverStrategic.OnErrorRecover -> MMKVOnErrorRecover +} + +@OptIn(ExperimentalForeignApi::class) +private fun darwinRecoverToCommon(value: DarwinMMKVRecoverStrategic): MMKVRecoverStrategic = when (value) { + MMKVOnErrorRecover -> MMKVRecoverStrategic.OnErrorRecover + else -> MMKVRecoverStrategic.OnErrorDiscard +} + +@OptIn(ExperimentalForeignApi::class) +private fun MMKVConfig.toDarwinCValue(): CValue = cValue { + mode = when { + this@toDarwinCValue.mode and MMKVMode.READ_ONLY != 0 -> MMKVReadOnly + this@toDarwinCValue.mode and MMKVMode.MULTI_PROCESS != 0 -> MMKVMultiProcess + else -> MMKVSingleProcess + } + aes256 = this@toDarwinCValue.aes256 + cryptKey = this@toDarwinCValue.cryptKey?.toNSData() + rootPath = this@toDarwinCValue.rootPath + expectedCapacity = this@toDarwinCValue.expectedCapacity.toULong() + enableKeyExpire = this@toDarwinCValue.enableKeyExpire?.let { NSNumber(bool = it) } + expiredInSeconds = this@toDarwinCValue.expiredInSeconds + enableCompareBeforeSet = this@toDarwinCValue.enableCompareBeforeSet + recover = this@toDarwinCValue.recover?.toDarwin() ?: MMKVOnErrorNotSet + itemSizeLimit = this@toDarwinCValue.itemSizeLimit +} + +// endregion + +// region NSData <-> ByteArray helpers + +@OptIn(ExperimentalForeignApi::class) +private fun ByteArray.toNSData(): NSData { + if (isEmpty()) return NSData() + return usePinned { pinned -> + NSData.create(bytes = pinned.addressOf(0), length = size.toULong()) + } +} + +@OptIn(ExperimentalForeignApi::class) +private fun NSData.toByteArray(): ByteArray { + val length = this.length.toInt() + if (length == 0) return ByteArray(0) + val bytes = ByteArray(length) + bytes.usePinned { pinned -> + memcpy(pinned.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) + } + return bytes +} + +@OptIn(ExperimentalForeignApi::class) +private fun String.toNSData(): NSData { + @Suppress("CAST_NEVER_SUCCEEDS") + return (this as NSString).dataUsingEncoding(NSUTF8StringEncoding) ?: NSData() +} + +// endregion + +// region Handler adapter + +/** + * Adapts the common [MMKVHandler] abstract class to the Darwin MMKVHandler ObjC protocol. + */ +@OptIn(ExperimentalForeignApi::class) +private class DarwinMMKVHandlerAdapter( + private val handler: MMKVHandler, +) : NSObject(), MMKVHandlerProtocol { + + override fun onMMKVCRCCheckFail(mmapID: String?): DarwinMMKVRecoverStrategic { + return handler.onMMKVCRCCheckFail(mmapID ?: "").toDarwin() + } + + override fun onMMKVFileLengthError(mmapID: String?): DarwinMMKVRecoverStrategic { + return handler.onMMKVFileLengthError(mmapID ?: "").toDarwin() + } + + override fun mmkvLogWithLevel(level: DarwinMMKVLogLevel, file: kotlinx.cinterop.CPointer?, line: Int, func: kotlinx.cinterop.CPointer?, message: String?) { + val fileName = file?.toKString() ?: "" + val funcName = func?.toKString() ?: "" + handler.mmkvLog(darwinLogLevelToCommon(level), fileName, line, funcName, message ?: "") + } + + override fun onMMKVContentChange(mmapID: String?) { + handler.onContentChangedByOuterProcess(mmapID ?: "") + } + + override fun onMMKVContentLoadSuccessfully(mmapID: String?) { + handler.onMMKVContentLoadSuccessfully(mmapID ?: "") + } +} + +// endregion diff --git a/KMP/sample/composeApp/build.gradle.kts b/KMP/sample/composeApp/build.gradle.kts new file mode 100644 index 00000000..ebb63c6a --- /dev/null +++ b/KMP/sample/composeApp/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + kotlin("multiplatform") + kotlin("native.cocoapods") + kotlin("plugin.compose") version "2.2.20" + id("com.android.application") + id("org.jetbrains.compose") version "1.8.1" +} + +kotlin { + androidTarget { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11) + } + } + + iosArm64() + iosSimulatorArm64() + iosX64() + + cocoapods { + summary = "MMKV KMP Sample App" + homepage = "https://github.com/Tencent/MMKV" + version = "1.0.0" + ios.deploymentTarget = "15.0" + podfile = project.file("../iosApp/Podfile") + + framework { + baseName = "ComposeApp" + isStatic = true + } + } + + sourceSets { + commonMain { + dependencies { + implementation(project(":mmkv")) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + } + } + androidMain { + dependencies { + implementation("androidx.activity:activity-compose:1.10.1") + } + } + } +} + +android { + namespace = "com.tencent.mmkv.sample" + compileSdk = 35 + + defaultConfig { + applicationId = "com.tencent.mmkv.sample" + minSdk = 24 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} diff --git a/KMP/sample/composeApp/src/androidMain/AndroidManifest.xml b/KMP/sample/composeApp/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..a9c193a9 --- /dev/null +++ b/KMP/sample/composeApp/src/androidMain/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/KMP/sample/composeApp/src/androidMain/kotlin/com/tencent/mmkv/sample/MainActivity.kt b/KMP/sample/composeApp/src/androidMain/kotlin/com/tencent/mmkv/sample/MainActivity.kt new file mode 100644 index 00000000..6036ba70 --- /dev/null +++ b/KMP/sample/composeApp/src/androidMain/kotlin/com/tencent/mmkv/sample/MainActivity.kt @@ -0,0 +1,20 @@ +package com.tencent.mmkv.sample + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import com.tencent.mmkv.kmp.MMKV +import com.tencent.mmkv.kmp.initialize + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Android-specific initialization (requires Context) + MMKV.initialize(this) + + setContent { + App() + } + } +} diff --git a/KMP/sample/composeApp/src/androidMain/kotlin/com/tencent/mmkv/sample/Platform.kt b/KMP/sample/composeApp/src/androidMain/kotlin/com/tencent/mmkv/sample/Platform.kt new file mode 100644 index 00000000..ad5cb9dc --- /dev/null +++ b/KMP/sample/composeApp/src/androidMain/kotlin/com/tencent/mmkv/sample/Platform.kt @@ -0,0 +1,5 @@ +package com.tencent.mmkv.sample + +actual fun platformSleep(millis: Long) { + Thread.sleep(millis) +} diff --git a/KMP/sample/composeApp/src/androidMain/res/values/themes.xml b/KMP/sample/composeApp/src/androidMain/res/values/themes.xml new file mode 100644 index 00000000..3f9c79bb --- /dev/null +++ b/KMP/sample/composeApp/src/androidMain/res/values/themes.xml @@ -0,0 +1,4 @@ + + +