提交 21ae9c08 authored 作者: 史晓晨's avatar 史晓晨

fix:转换kotlin,修复无法获取设备id问题

上级 1b4c625a
package com.clx.devideId.device_id_plugin; package com.clx.devideId.device_id_plugin
import android.annotation.SuppressLint; import android.annotation.SuppressLint
import android.content.Context; import android.content.Context
import android.provider.Settings; import android.provider.Settings
import android.text.TextUtils; import java.util.UUID
import java.util.UUID;
/** /**
* <pre> * <pre>
...@@ -14,126 +13,132 @@ import java.util.UUID; ...@@ -14,126 +13,132 @@ import java.util.UUID;
* desc : utils about device * desc : utils about device
* </pre> * </pre>
*/ */
public final class DeviceUtils { object DeviceUtils {
@SuppressLint("StaticFieldLeak")
private var mContext: Context? = null
private DeviceUtils() { private const val KEY_UDID = "KEY_UDID"
throw new UnsupportedOperationException("u can't instantiate me...");
}
@SuppressLint("StaticFieldLeak") @Volatile
private static Context mContext; private var udid: String? = null
public static void init(Context context) { /**
mContext = context; * 初始化设备工具类
*/
fun init(context: Context?) {
if (context == null) return
mContext = context.applicationContext
} }
/** /**
* Return the android id of device. * 获取 Android ID
*
* @return the android id of device
*/ */
@SuppressLint("HardwareIds") @SuppressLint("HardwareIds")
public static String getAndroidID() { fun getAndroidID(): String {
String id = Settings.Secure.getString( val context = mContext ?: return ""
mContext.getContentResolver(),
Settings.Secure.ANDROID_ID
);
if ("9774d56d682e549c".equals(id)) return "";
return id == null ? "" : id;
}
private static final String KEY_UDID = "KEY_UDID"; val id = Settings.Secure.getString(
private volatile static String udid; context.contentResolver, Settings.Secure.ANDROID_ID
)
return when {
id == "9774d56d682e549c" -> "" // 过滤无效 Android ID
id.isNullOrEmpty() -> ""
else -> id
}
}
/** /**
* Return the unique device id. * 获取唯一设备 ID(默认前缀空、使用缓存)
* <pre>{1}{UUID(macAddress)}</pre>
* <pre>{2}{UUID(androidId )}</pre>
* <pre>{9}{UUID(random )}</pre>
*
* @return the unique device id
*/ */
public static String getUniqueDeviceId() { fun getUniqueDeviceId(): String {
return getUniqueDeviceId("", true); return getUniqueDeviceId("", true)
} }
/** /**
* Return the unique device id. * 获取唯一设备 ID(指定前缀、使用缓存)
* <pre>android 10 deprecated {prefix}{1}{UUID(macAddress)}</pre> * @param prefix 设备 ID 前缀
* <pre>{prefix}{2}{UUID(androidId )}</pre>
* <pre>{prefix}{9}{UUID(random )}</pre>
*
* @param prefix The prefix of the unique device id.
* @return the unique device id
*/ */
public static String getUniqueDeviceId(String prefix) { fun getUniqueDeviceId(prefix: String?): String {
return getUniqueDeviceId(prefix, true); return getUniqueDeviceId(prefix, true)
} }
/** /**
* Return the unique device id. * 获取唯一设备 ID(默认前缀、指定是否使用缓存)
* <pre>{1}{UUID(macAddress)}</pre> * @param useCache 是否使用缓存
* <pre>{2}{UUID(androidId )}</pre>
* <pre>{9}{UUID(random )}</pre>
*
* @param useCache True to use cache, false otherwise.
* @return the unique device id
*/ */
public static String getUniqueDeviceId(boolean useCache) { fun getUniqueDeviceId(useCache: Boolean): String {
return getUniqueDeviceId("", useCache); return getUniqueDeviceId("", useCache)
} }
/** /**
* Return the unique device id. * 获取唯一设备 ID(核心方法)
* <pre>android 10 deprecated {prefix}{1}{UUID(macAddress)}</pre> * @param prefix 设备 ID 前缀
* <pre>{prefix}{2}{UUID(androidId )}</pre> * @param useCache 是否使用缓存
* <pre>{prefix}{9}{UUID(random )}</pre>
*
* @param prefix The prefix of the unique device id.
* @param useCache True to use cache, false otherwise.
* @return the unique device id
*/ */
public static String getUniqueDeviceId(String prefix, boolean useCache) { fun getUniqueDeviceId(prefix: String?, useCache: Boolean): String {
val safePrefix = prefix ?: ""
if (!useCache) { if (!useCache) {
return getUniqueDeviceIdReal(prefix); return getUniqueDeviceIdReal(safePrefix)
} }
if (udid == null) { if (udid == null) {
synchronized (DeviceUtils.class) { synchronized(DeviceUtils::class.java) {
if (udid == null) { if (udid == null) {
final String id = SPUtils.getInstance().getString(KEY_UDID, null); val cachedId = SPUtils.getInstance().getString(KEY_UDID, null)
if (id != null) { if (cachedId != null) {
udid = id; udid = cachedId
return udid; return udid!!
} }
return getUniqueDeviceIdReal(prefix); // 缓存不存在则生成新的 UDID
return getUniqueDeviceIdReal(safePrefix)
} }
} }
} }
return udid; return udid ?: ""
} }
private static String getUniqueDeviceIdReal(String prefix) { /**
try { * 生成真实的唯一设备 ID(无缓存)
final String androidId = getAndroidID(); */
if (!TextUtils.isEmpty(androidId)) { private fun getUniqueDeviceIdReal(prefix: String): String {
return saveUdid(prefix + 2, androidId); return try {
val androidId = getAndroidID()
if (androidId.isNotEmpty()) {
saveUdid(prefix + 2, androidId)
} else {
saveUdid(prefix + 9, "")
}
} catch (ignore: Exception) {
// 异常时生成随机 UUID
saveUdid(prefix + 9, "")
} }
} catch (Exception ignore) {/**/}
return saveUdid(prefix + 9, "");
} }
private static String saveUdid(String prefix, String id) { /**
udid = getUdid(prefix, id); * 保存 UDID 到 SP 并更新内存缓存(修复:id 判空 + SPUtils 操作加 try-catch)
SPUtils.getInstance().put(KEY_UDID, udid); */
return udid; private fun saveUdid(prefix: String, id: String?): String {
val safeId = id ?: ""
udid = getUdid(prefix, safeId)
SPUtils.getInstance().put(KEY_UDID, udid!!)
return udid ?: ""
} }
private static String getUdid(String prefix, String id) { /**
if (id.equals("")) { * 生成 UDID 字符串
return prefix + UUID.randomUUID().toString().replace("-", ""); */
private fun getUdid(prefix: String?, id: String?): String {
val safePrefix = prefix ?: ""
val safeId = id ?: ""
return if (safeId.isEmpty()) {
// ID 为空时生成随机 UUID
safePrefix + UUID.randomUUID().toString().replace("-", "")
} else {
// ID 不为空时基于 ID 生成 UUID
safePrefix + UUID.nameUUIDFromBytes(safeId.toByteArray()).toString().replace("-", "")
} }
return prefix + UUID.nameUUIDFromBytes(id.getBytes()).toString().replace("-", "");
} }
}
}
\ No newline at end of file
package com.clx.devideId.device_id_plugin; package com.clx.devideId.device_id_plugin
import android.annotation.SuppressLint; import android.annotation.SuppressLint
import android.content.Context; import android.content.Context
import android.content.SharedPreferences; import android.content.SharedPreferences
import androidx.annotation.NonNull
import androidx.annotation.NonNull; import java.util.Collections
import java.util.WeakHashMap
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/** /**
* <pre> * <pre>
...@@ -20,406 +16,292 @@ import java.util.Set; ...@@ -20,406 +16,292 @@ import java.util.Set;
* </pre> * </pre>
*/ */
@SuppressLint("ApplySharedPref") @SuppressLint("ApplySharedPref")
public final class SPUtils { object SPUtils {
// 替换 Java 的 HashMap,使用 WeakHashMap 更贴合 Kotlin 内存管理
private static final Map<String, SPUtils> SP_UTILS_MAP = new HashMap<>(); private val SP_UTILS_MAP = WeakHashMap<String, SPUtilsHelper>()
private final SharedPreferences sp;
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private static Context mContext; private lateinit var mContext: Context
public static void init(Context context) { /**
mContext = context; * 初始化 SP 工具类(必须先调用)
*/
fun init(context: Context) {
mContext = context.applicationContext // 强制使用 ApplicationContext 避免内存泄漏
} }
public static SPUtils getInstance() { /**
return getInstance("device_info_utils"); * 获取默认 SP 实例(name = "device_info_utils")
*/
fun getInstance(): SPUtilsHelper {
return getInstance("device_info_utils")
} }
/** /**
* Return the single {@link SPUtils} instance * 获取指定名称的 SP 实例
* * @param spName SP 文件名
* @param spName The name of sp.
* @return the single {@link SPUtils} instance
*/ */
public static SPUtils getInstance(String spName) { fun getInstance(spName: String): SPUtilsHelper {
if (isSpace(spName)) spName = "spUtils"; val name = if (spName.isSpace()) "spUtils" else spName
SPUtils spUtils = SP_UTILS_MAP.get(spName); return SP_UTILS_MAP[name] ?: synchronized(SPUtils::class.java) {
if (spUtils == null) { SP_UTILS_MAP[name] ?: SPUtilsHelper(name).also { SP_UTILS_MAP[name] = it }
synchronized (SPUtils.class) {
spUtils = SP_UTILS_MAP.get(spName);
if (spUtils == null) {
spUtils = new SPUtils(spName);
SP_UTILS_MAP.put(spName, spUtils);
}
} }
} }
return spUtils;
}
private SPUtils(final String spName) { /**
sp = mContext.getSharedPreferences(spName, Context.MODE_PRIVATE); * SP 操作助手类(封装具体的 SP 读写逻辑)
*/
class SPUtilsHelper(private val spName: String) {
private val sp: SharedPreferences by lazy {
check(::mContext.isInitialized) { "SPUtils 未初始化!请先调用 SPUtils.init(context)" }
mContext.getSharedPreferences(spName, Context.MODE_PRIVATE)
} }
// ===================== String 类型 =====================
/** /**
* Put the string value in sp. * 保存字符串
*
* @param key The key of sp.
* @param value The value of sp.
*/ */
public void put(@NonNull final String key, final String value) { fun put(@NonNull key: String, value: String) {
put(key, value, false); put(key, value, false)
} }
/** /**
* Put the string value in sp. * 保存字符串
* * @param isCommit true: commit() 同步保存 | false: apply() 异步保存
* @param key The key of sp.
* @param value The value of sp.
* @param isCommit True to use {@link SharedPreferences.Editor#commit()},
* false to use {@link SharedPreferences.Editor#apply()}
*/ */
public void put(@NonNull final String key, final String value, final boolean isCommit) { fun put(@NonNull key: String, value: String, isCommit: Boolean) {
if (isCommit) { val editor = sp.edit().putString(key, value)
sp.edit().putString(key, value).commit(); if (isCommit) editor.commit() else editor.apply()
} else {
sp.edit().putString(key, value).apply();
}
} }
/** /**
* Return the string value in sp. * 获取字符串(默认值:"")
*
* @param key The key of sp.
* @return the string value if sp exists or {@code ""} otherwise
*/ */
public String getString(@NonNull final String key) { fun getString(@NonNull key: String): String? {
return getString(key, ""); return getString(key, "")
} }
/** /**
* Return the string value in sp. * 获取字符串
* * @param defaultValue 无值时的默认值
* @param key The key of sp.
* @param defaultValue The default value if the sp doesn't exist.
* @return the string value if sp exists or {@code defaultValue} otherwise
*/ */
public String getString(@NonNull final String key, final String defaultValue) { fun getString(@NonNull key: String, defaultValue: String?): String? {
return sp.getString(key, defaultValue); return sp.getString(key, defaultValue) ?: defaultValue
} }
// ===================== Int 类型 =====================
/** /**
* Put the int value in sp. * 保存 Int 类型值
*
* @param key The key of sp.
* @param value The value of sp.
*/ */
public void put(@NonNull final String key, final int value) { fun put(@NonNull key: String, value: Int) {
put(key, value, false); put(key, value, false)
} }
/** /**
* Put the int value in sp. * 保存 Int 类型值
* * @param isCommit true: commit() 同步保存 | false: apply() 异步保存
* @param key The key of sp.
* @param value The value of sp.
* @param isCommit True to use {@link SharedPreferences.Editor#commit()},
* false to use {@link SharedPreferences.Editor#apply()}
*/ */
public void put(@NonNull final String key, final int value, final boolean isCommit) { fun put(@NonNull key: String, value: Int, isCommit: Boolean) {
if (isCommit) { val editor = sp.edit().putInt(key, value)
sp.edit().putInt(key, value).commit(); if (isCommit) editor.commit() else editor.apply()
} else {
sp.edit().putInt(key, value).apply();
}
} }
/** /**
* Return the int value in sp. * 获取 Int 类型值(默认值:-1)
*
* @param key The key of sp.
* @return the int value if sp exists or {@code -1} otherwise
*/ */
public int getInt(@NonNull final String key) { fun getInt(@NonNull key: String): Int {
return getInt(key, -1); return getInt(key, -1)
} }
/** /**
* Return the int value in sp. * 获取 Int 类型值
* * @param defaultValue 无值时的默认值
* @param key The key of sp.
* @param defaultValue The default value if the sp doesn't exist.
* @return the int value if sp exists or {@code defaultValue} otherwise
*/ */
public int getInt(@NonNull final String key, final int defaultValue) { fun getInt(@NonNull key: String, defaultValue: Int): Int {
return sp.getInt(key, defaultValue); return sp.getInt(key, defaultValue)
} }
// ===================== Long 类型 =====================
/** /**
* Put the long value in sp. * 保存 Long 类型值
*
* @param key The key of sp.
* @param value The value of sp.
*/ */
public void put(@NonNull final String key, final long value) { fun put(@NonNull key: String, value: Long) {
put(key, value, false); put(key, value, false)
} }
/** /**
* Put the long value in sp. * 保存 Long 类型值
* * @param isCommit true: commit() 同步保存 | false: apply() 异步保存
* @param key The key of sp.
* @param value The value of sp.
* @param isCommit True to use {@link SharedPreferences.Editor#commit()},
* false to use {@link SharedPreferences.Editor#apply()}
*/ */
public void put(@NonNull final String key, final long value, final boolean isCommit) { fun put(@NonNull key: String, value: Long, isCommit: Boolean) {
if (isCommit) { val editor = sp.edit().putLong(key, value)
sp.edit().putLong(key, value).commit(); if (isCommit) editor.commit() else editor.apply()
} else {
sp.edit().putLong(key, value).apply();
}
} }
/** /**
* Return the long value in sp. * 获取 Long 类型值(默认值:-1L)
*
* @param key The key of sp.
* @return the long value if sp exists or {@code -1} otherwise
*/ */
public long getLong(@NonNull final String key) { fun getLong(@NonNull key: String): Long {
return getLong(key, -1L); return getLong(key, -1L)
} }
/** /**
* Return the long value in sp. * 获取 Long 类型值
* * @param defaultValue 无值时的默认值
* @param key The key of sp.
* @param defaultValue The default value if the sp doesn't exist.
* @return the long value if sp exists or {@code defaultValue} otherwise
*/ */
public long getLong(@NonNull final String key, final long defaultValue) { fun getLong(@NonNull key: String, defaultValue: Long): Long {
return sp.getLong(key, defaultValue); return sp.getLong(key, defaultValue)
} }
// ===================== Float 类型 =====================
/** /**
* Put the float value in sp. * 保存 Float 类型值
*
* @param key The key of sp.
* @param value The value of sp.
*/ */
public void put(@NonNull final String key, final float value) { fun put(@NonNull key: String, value: Float) {
put(key, value, false); put(key, value, false)
} }
/** /**
* Put the float value in sp. * 保存 Float 类型值
* * @param isCommit true: commit() 同步保存 | false: apply() 异步保存
* @param key The key of sp.
* @param value The value of sp.
* @param isCommit True to use {@link SharedPreferences.Editor#commit()},
* false to use {@link SharedPreferences.Editor#apply()}
*/ */
public void put(@NonNull final String key, final float value, final boolean isCommit) { fun put(@NonNull key: String, value: Float, isCommit: Boolean) {
if (isCommit) { val editor = sp.edit().putFloat(key, value)
sp.edit().putFloat(key, value).commit(); if (isCommit) editor.commit() else editor.apply()
} else {
sp.edit().putFloat(key, value).apply();
}
} }
/** /**
* Return the float value in sp. * 获取 Float 类型值(默认值:-1f)
*
* @param key The key of sp.
* @return the float value if sp exists or {@code -1f} otherwise
*/ */
public float getFloat(@NonNull final String key) { fun getFloat(@NonNull key: String): Float {
return getFloat(key, -1f); return getFloat(key, -1f)
} }
/** /**
* Return the float value in sp. * 获取 Float 类型值
* * @param defaultValue 无值时的默认值
* @param key The key of sp.
* @param defaultValue The default value if the sp doesn't exist.
* @return the float value if sp exists or {@code defaultValue} otherwise
*/ */
public float getFloat(@NonNull final String key, final float defaultValue) { fun getFloat(@NonNull key: String, defaultValue: Float): Float {
return sp.getFloat(key, defaultValue); return sp.getFloat(key, defaultValue)
} }
// ===================== Boolean 类型 =====================
/** /**
* Put the boolean value in sp. * 保存 Boolean 类型值
*
* @param key The key of sp.
* @param value The value of sp.
*/ */
public void put(@NonNull final String key, final boolean value) { fun put(@NonNull key: String, value: Boolean) {
put(key, value, false); put(key, value, false)
} }
/** /**
* Put the boolean value in sp. * 保存 Boolean 类型值
* * @param isCommit true: commit() 同步保存 | false: apply() 异步保存
* @param key The key of sp.
* @param value The value of sp.
* @param isCommit True to use {@link SharedPreferences.Editor#commit()},
* false to use {@link SharedPreferences.Editor#apply()}
*/ */
public void put(@NonNull final String key, final boolean value, final boolean isCommit) { fun put(@NonNull key: String, value: Boolean, isCommit: Boolean) {
if (isCommit) { val editor = sp.edit().putBoolean(key, value)
sp.edit().putBoolean(key, value).commit(); if (isCommit) editor.commit() else editor.apply()
} else {
sp.edit().putBoolean(key, value).apply();
}
} }
/** /**
* Return the boolean value in sp. * 获取 Boolean 类型值(默认值:false)
*
* @param key The key of sp.
* @return the boolean value if sp exists or {@code false} otherwise
*/ */
public boolean getBoolean(@NonNull final String key) { fun getBoolean(@NonNull key: String): Boolean {
return getBoolean(key, false); return getBoolean(key, false)
} }
/** /**
* Return the boolean value in sp. * 获取 Boolean 类型值
* * @param defaultValue 无值时的默认值
* @param key The key of sp.
* @param defaultValue The default value if the sp doesn't exist.
* @return the boolean value if sp exists or {@code defaultValue} otherwise
*/ */
public boolean getBoolean(@NonNull final String key, final boolean defaultValue) { fun getBoolean(@NonNull key: String, defaultValue: Boolean): Boolean {
return sp.getBoolean(key, defaultValue); return sp.getBoolean(key, defaultValue)
} }
// ===================== Set<String> 类型 =====================
/** /**
* Put the set of string value in sp. * 保存 String 集合
*
* @param key The key of sp.
* @param value The value of sp.
*/ */
public void put(@NonNull final String key, final Set<String> value) { fun put(@NonNull key: String, value: Set<String>) {
put(key, value, false); put(key, value, false)
} }
/** /**
* Put the set of string value in sp. * 保存 String 集合
* * @param isCommit true: commit() 同步保存 | false: apply() 异步保存
* @param key The key of sp.
* @param value The value of sp.
* @param isCommit True to use {@link SharedPreferences.Editor#commit()},
* false to use {@link SharedPreferences.Editor#apply()}
*/ */
public void put(@NonNull final String key, fun put(@NonNull key: String, value: Set<String>, isCommit: Boolean) {
final Set<String> value, val editor = sp.edit().putStringSet(key, value)
final boolean isCommit) { if (isCommit) editor.commit() else editor.apply()
if (isCommit) {
sp.edit().putStringSet(key, value).commit();
} else {
sp.edit().putStringSet(key, value).apply();
}
} }
/** /**
* Return the set of string value in sp. * 获取 String 集合(默认值:空集合)
*
* @param key The key of sp.
* @return the set of string value if sp exists
* or {@code Collections.<String>emptySet()} otherwise
*/ */
public Set<String> getStringSet(@NonNull final String key) { fun getStringSet(@NonNull key: String): Set<String> {
return getStringSet(key, Collections.<String>emptySet()); return getStringSet(key, emptySet())
} }
/** /**
* Return the set of string value in sp. * 获取 String 集合
* * @param defaultValue 无值时的默认值
* @param key The key of sp.
* @param defaultValue The default value if the sp doesn't exist.
* @return the set of string value if sp exists or {@code defaultValue} otherwise
*/ */
public Set<String> getStringSet(@NonNull final String key, fun getStringSet(@NonNull key: String, defaultValue: Set<String>): Set<String> {
final Set<String> defaultValue) { return sp.getStringSet(key, defaultValue) ?: Collections.emptySet()
return sp.getStringSet(key, defaultValue);
} }
// ===================== 通用方法 =====================
/** /**
* Return all values in sp. * 获取 SP 中所有键值对
*
* @return all values in sp
*/ */
public Map<String, ?> getAll() { fun getAll(): Map<String, *> {
return sp.getAll(); return sp.all
} }
/** /**
* Return whether the sp contains the preference. * 判断 SP 中是否包含指定 key
*
* @param key The key of sp.
* @return {@code true}: yes<br>{@code false}: no
*/ */
public boolean contains(@NonNull final String key) { fun contains(@NonNull key: String): Boolean {
return sp.contains(key); return sp.contains(key)
} }
/** /**
* Remove the preference in sp. * 移除 SP 中指定 key 的值
*
* @param key The key of sp.
*/ */
public void remove(@NonNull final String key) { fun remove(@NonNull key: String) {
remove(key, false); remove(key, false)
} }
/** /**
* Remove the preference in sp. * 移除 SP 中指定 key 的值
* * @param isCommit true: commit() 同步移除 | false: apply() 异步移除
* @param key The key of sp.
* @param isCommit True to use {@link SharedPreferences.Editor#commit()},
* false to use {@link SharedPreferences.Editor#apply()}
*/ */
public void remove(@NonNull final String key, final boolean isCommit) { fun remove(@NonNull key: String, isCommit: Boolean) {
if (isCommit) { val editor = sp.edit().remove(key)
sp.edit().remove(key).commit(); if (isCommit) editor.commit() else editor.apply()
} else {
sp.edit().remove(key).apply();
}
} }
/** /**
* Remove all preferences in sp. * 清空 SP 中所有数据
*/ */
public void clear() { fun clear() {
clear(false); clear(false)
} }
/** /**
* Remove all preferences in sp. * 清空 SP 中所有数据
* * @param isCommit true: commit() 同步清空 | false: apply() 异步清空
* @param isCommit True to use {@link SharedPreferences.Editor#commit()},
* false to use {@link SharedPreferences.Editor#apply()}
*/ */
public void clear(final boolean isCommit) { fun clear(isCommit: Boolean) {
if (isCommit) { val editor = sp.edit().clear()
sp.edit().clear().commit(); if (isCommit) editor.commit() else editor.apply()
} else {
sp.edit().clear().apply();
} }
} }
private static boolean isSpace(final String s) { /**
if (s == null) return true; * 扩展函数:判断字符串是否为空/全是空白字符
for (int i = 0, len = s.length(); i < len; ++i) { */
if (!Character.isWhitespace(s.charAt(i))) { private fun String?.isSpace(): Boolean {
return false; if (this == null) return true
} return this.all { it.isWhitespace() }
}
return true;
} }
} }
\ No newline at end of file
...@@ -202,10 +202,10 @@ packages: ...@@ -202,10 +202,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.10.1" version: "1.10.2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论