>().javaType.toString() }
8 |
9 | /*****获取默认值***/
10 | internal val KClass<*>.defaultValue
11 | get() = when (this) {
12 | Int::class -> 0
13 | Long::class -> 0L
14 | Float::class -> 0.0F
15 | Boolean::class -> false
16 | else -> null
17 | }
--------------------------------------------------------------------------------
/preferences-core/src/main/java/com/forjrking/preferences/provide/MultiProcessSharedPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences.provide
2 |
3 | import android.content.*
4 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener
5 | import android.content.pm.PackageManager
6 | import android.database.Cursor
7 | import android.database.MatrixCursor
8 | import android.net.Uri
9 | import android.os.Bundle
10 | import android.util.LruCache
11 | import java.lang.ref.SoftReference
12 |
13 | /**
14 | * 使用ContentProvider实现多进程SharedPreferences读写;
15 | * 1、ContentProvider天生支持多进程访问;
16 | * 2、使用内部私有BroadcastReceiver实现多进程OnSharedPreferenceChangeListener监听;
17 | *
18 | *
19 | * 使用方法:AndroidManifest.xml中添加provider申明:
20 | *
21 | * <provider android:name="com.tencent.mm.sdk.patformtools.MultiProcessSharedPreferences"
22 | * android:authorities="com.tencent.mm.sdk.patformtools.MultiProcessSharedPreferences"
23 | * android:exported="false" />
24 | * <!-- authorities属性里面最好使用包名做前缀,apk在安装时authorities同名的provider需要校验签名,否则无法安装;--!/>
25 |
*
26 | *
27 | *
28 | * ContentProvider方式实现要注意:
29 | * 1、当ContentProvider所在进程android.os.Process.killProcess(pid)时,会导致整个应用程序完全意外退出或者ContentProvider所在进程重启;
30 | * 重启报错信息:Acquiring provider for user 0: existing object's process dead;
31 | * 2、如果设备处在“安全模式”下,只有系统自带的ContentProvider才能被正常解析使用,因此put值时默认返回false,get值时默认返回null;
32 | *
33 | *
34 | * 其他方式实现SharedPreferences的问题:
35 | * 使用FileLock和FileObserver也可以实现多进程SharedPreferences读写,但是会有兼容性问题:
36 | * 1、某些设备上卸载程序时锁文件无法删除导致卸载残留,进而导致无法重新安装该程序(报INSTALL_FAILED_UID_CHANGED错误);
37 | * 2、某些设备上FileLock会导致僵尸进程出现进而导致耗电;
38 | * 3、僵尸进程出现后,正常进程的FileLock会一直阻塞等待僵尸进程中的FileLock释放,导致进程一直阻塞;
39 | *
40 | * @author seven456@gmail.com
41 | * @version 1.0
42 | * @since JDK1.6
43 | */
44 | class MultiProcessSharedPreferences : ContentProvider, SharedPreferences {
45 | private var mContext: Context? = null
46 | private var mName: String? = null
47 | private var mKeyAlias: String? = null
48 | private var mIsSafeMode = false
49 | private var mListeners: MutableList>? = null
50 | private var mReceiver: BroadcastReceiver? = null
51 | private var mUriMatcher: UriMatcher? = null
52 | private var mListenersCount: MutableMap? = null
53 |
54 | private val mLruCache = object : LruCache(3) {
55 | override fun create(key: String?): SharedPreferences {
56 | val param = key!!.split(";")
57 | return compatSharedPreferences(context!!, param[0], param[1])
58 | }
59 |
60 | fun getSharedPreferences(name: String, keyAlias: String?) =
61 | get("$name;${keyAlias.orEmpty()}")
62 | }
63 |
64 | constructor()
65 | private constructor(context: Context, name: String, keyAlias: String? = null) {
66 | mContext = context
67 | mName = name
68 | mKeyAlias = keyAlias
69 | }
70 |
71 | private object ReflectionUtil {
72 | fun contentValuesNewInstance(values: HashMap?): ContentValues {
73 | return try {
74 | val c = ContentValues::class.java.getDeclaredConstructor(
75 | *arrayOf>(
76 | HashMap::class.java
77 | )
78 | ) // hide
79 | c.isAccessible = true
80 | c.newInstance(values)
81 | } catch (e: Exception) {
82 | throw RuntimeException(e)
83 | }
84 | }
85 | }
86 |
87 | private fun checkInitAuthority(context: Context?) {
88 | if (AUTHORITY_URI == null) {
89 | var authority: String? = null
90 | var authorityUri = AUTHORITY_URI
91 | synchronized(this@MultiProcessSharedPreferences) {
92 | if (authorityUri == null) {
93 | authority = queryAuthority(context)
94 | authorityUri = Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + authority)
95 | }
96 | requireNotNull(authority) { "'AUTHORITY' initialize failed." }
97 | }
98 | AUTHORITY = authority
99 | AUTHORITY_URI = authorityUri
100 | }
101 | }
102 |
103 | override fun getAll(): Map? {
104 | return getValue(PATH_GET_ALL, null, null) as Map?
105 | }
106 |
107 | override fun getString(key: String, defValue: String?): String? {
108 | val v = getValue(PATH_GET_STRING, key, defValue) as String?
109 | return v ?: defValue
110 | }
111 |
112 | // @Override // Android 3.0
113 | override fun getStringSet(key: String, defValues: Set?): Set? {
114 | synchronized(this) {
115 | val v = getValue(PATH_GET_SET_STRING, key, defValues) as Set?
116 | return v ?: defValues
117 | }
118 | }
119 |
120 | override fun getInt(key: String, defValue: Int): Int {
121 | val v = getValue(PATH_GET_INT, key, defValue) as Int?
122 | return v ?: defValue
123 | }
124 |
125 | override fun getLong(key: String, defValue: Long): Long {
126 | val v = getValue(PATH_GET_LONG, key, defValue) as Long?
127 | return v ?: defValue
128 | }
129 |
130 | override fun getFloat(key: String, defValue: Float): Float {
131 | val v = getValue(PATH_GET_FLOAT, key, defValue) as Float?
132 | return v ?: defValue
133 | }
134 |
135 | override fun getBoolean(key: String, defValue: Boolean): Boolean {
136 | val v = getValue(PATH_GET_BOOLEAN, key, defValue) as Boolean?
137 | return v ?: defValue
138 | }
139 |
140 | override fun contains(key: String): Boolean {
141 | val v = getValue(PATH_CONTAINS, key, null) as Boolean?
142 | return v ?: false
143 | }
144 |
145 | override fun edit(): SharedPreferences.Editor {
146 | return EditorImpl()
147 | }
148 |
149 | override fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
150 | synchronized(this) {
151 | if (mListeners == null) {
152 | mListeners = ArrayList()
153 | }
154 | val result = getValue(
155 | PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER,
156 | null,
157 | false
158 | ) as Boolean?
159 | if (result != null && result) {
160 | mListeners!!.add(SoftReference(listener))
161 | if (mReceiver == null) {
162 | mReceiver = object : BroadcastReceiver() {
163 | override fun onReceive(context: Context, intent: Intent) {
164 | val name = intent.getStringExtra(KEY_NAME)
165 | val keysModified = intent.getSerializableExtra(KEY) as List?
166 | if (mName == name && keysModified != null) {
167 | val listeners =
168 | mutableListOf>()
169 | synchronized(this@MultiProcessSharedPreferences) {
170 | listeners.addAll(mListeners!!)
171 | }
172 | for (i in keysModified.indices.reversed()) {
173 | val key = keysModified[i]
174 | for (srlistener in listeners) {
175 | srlistener.get()?.onSharedPreferenceChanged(
176 | this@MultiProcessSharedPreferences,
177 | key
178 | )
179 | }
180 | }
181 | }
182 | }
183 | }
184 | mContext!!.registerReceiver(mReceiver, IntentFilter(makeAction(mName)))
185 | }
186 | }
187 | }
188 | }
189 |
190 | override fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
191 | synchronized(this) {
192 | getValue(PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER, null, false)
193 | if (mListeners != null) {
194 | val removing: MutableList> =
195 | ArrayList()
196 | for (srlistener in mListeners!!) {
197 | val listenerFromSR = srlistener.get()
198 | if (listenerFromSR != null && listenerFromSR == listener) {
199 | removing.add(srlistener)
200 | }
201 | }
202 | for (srlistener in removing) {
203 | mListeners!!.remove(srlistener)
204 | }
205 | if (mListeners!!.isEmpty() && mReceiver != null) {
206 | mContext!!.unregisterReceiver(mReceiver)
207 | mReceiver = null
208 | mListeners = null
209 | }
210 | }
211 | }
212 | }
213 |
214 | inner class EditorImpl : SharedPreferences.Editor {
215 | private val mModified: MutableMap = HashMap()
216 | private var mClear = false
217 | override fun putString(key: String, value: String?): SharedPreferences.Editor {
218 | synchronized(this) {
219 | mModified[key] = value
220 | return this
221 | }
222 | }
223 |
224 | // @Override // Android 3.0
225 | override fun putStringSet(key: String, values: Set?): SharedPreferences.Editor {
226 | synchronized(this) {
227 | mModified[key] = if (values == null) null else HashSet(values)
228 | return this
229 | }
230 | }
231 |
232 | override fun putInt(key: String, value: Int): SharedPreferences.Editor {
233 | synchronized(this) {
234 | mModified[key] = value
235 | return this
236 | }
237 | }
238 |
239 | override fun putLong(key: String, value: Long): SharedPreferences.Editor {
240 | synchronized(this) {
241 | mModified[key] = value
242 | return this
243 | }
244 | }
245 |
246 | override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
247 | synchronized(this) {
248 | mModified[key] = value
249 | return this
250 | }
251 | }
252 |
253 | override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
254 | synchronized(this) {
255 | mModified[key] = value
256 | return this
257 | }
258 | }
259 |
260 | override fun remove(key: String): SharedPreferences.Editor {
261 | synchronized(this) {
262 | mModified[key] = null
263 | return this
264 | }
265 | }
266 |
267 | override fun clear(): SharedPreferences.Editor {
268 | synchronized(this) {
269 | mClear = true
270 | return this
271 | }
272 | }
273 |
274 | override fun apply() {
275 | setValue(PATH_APPLY)
276 | }
277 |
278 | override fun commit(): Boolean {
279 | return setValue(PATH_COMMIT)
280 | }
281 |
282 | private fun setValue(pathSegment: String): Boolean {
283 | return if (mIsSafeMode) { // 如果设备处在“安全模式”,返回false;
284 | false
285 | } else {
286 | synchronized(this@MultiProcessSharedPreferences) {
287 | checkInitAuthority(mContext)
288 | val selectionArgs = arrayOf(mKeyAlias, mClear.toString())
289 | synchronized(this) {
290 | val uri = Uri.withAppendedPath(
291 | Uri.withAppendedPath(AUTHORITY_URI, mName), pathSegment
292 | )
293 | val values = ReflectionUtil.contentValuesNewInstance(
294 | mModified as HashMap
295 | )
296 | return mContext!!.contentResolver.update(
297 | uri,
298 | values,
299 | null,
300 | selectionArgs
301 | ) > 0
302 | }
303 | }
304 | }
305 | }
306 | }
307 |
308 | private fun getValue(pathSegment: String, key: String?, defValue: Any?): Any? {
309 | return if (mIsSafeMode) { // 如果设备处在“安全模式”,返回null;
310 | null
311 | } else {
312 | checkInitAuthority(mContext)
313 | var v: Any? = null
314 | val uri = Uri.withAppendedPath(Uri.withAppendedPath(AUTHORITY_URI, mName), pathSegment)
315 | val selectionArgs = arrayOf(mKeyAlias, key, defValue?.toString())
316 | val cursor = mContext!!.contentResolver.query(uri, null, null, selectionArgs, null)
317 | cursor?.use {
318 | try {
319 | val bundle = it.extras
320 | if (bundle != null) {
321 | v = bundle[KEY]
322 | bundle.clear()
323 | }
324 | } catch (e: Exception) {
325 | //not required
326 | }
327 | }
328 | v ?: defValue
329 | }
330 | }
331 |
332 | private fun makeAction(name: String?): String {
333 | return String.format("%1\$s_%2\$s", MultiProcessSharedPreferences::class.java.name, name)
334 | }
335 |
336 | override fun onCreate(): Boolean {
337 | mIsSafeMode = context?.packageManager?.isSafeMode == true
338 | checkInitAuthority(context)
339 | mUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
340 | addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_ALL, GET_ALL)
341 | addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_SET_STRING, GET_SET_STRING)
342 | addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_STRING, GET_STRING)
343 | addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_INT, GET_INT)
344 | addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_LONG, GET_LONG)
345 | addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_FLOAT, GET_FLOAT)
346 | addURI(AUTHORITY, PATH_WILDCARD + PATH_GET_BOOLEAN, GET_BOOLEAN)
347 | addURI(AUTHORITY, PATH_WILDCARD + PATH_CONTAINS, CONTAINS)
348 | addURI(AUTHORITY, PATH_WILDCARD + PATH_APPLY, APPLY)
349 | addURI(AUTHORITY, PATH_WILDCARD + PATH_COMMIT, COMMIT)
350 | addURI(
351 | AUTHORITY, PATH_WILDCARD + PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER,
352 | REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER
353 | )
354 | addURI(
355 | AUTHORITY, PATH_WILDCARD + PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER,
356 | UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER
357 | )
358 | }
359 | return true
360 | }
361 |
362 | override fun query(
363 | uri: Uri,
364 | projection: Array?,
365 | selection: String?,
366 | selectionArgs: Array?,
367 | sortOrder: String?
368 | ): Cursor? {
369 | val name = uri.pathSegments[0]
370 | val keyAlias = selectionArgs!![0]
371 | val key = selectionArgs[1]
372 | val defValue = selectionArgs[2]
373 | val bundle = Bundle()
374 | when (mUriMatcher!!.match(uri)) {
375 | GET_ALL -> bundle.putSerializable(
376 | KEY,
377 | mLruCache.getSharedPreferences(name, keyAlias).all as HashMap
378 | )
379 |
380 | GET_SET_STRING -> bundle.putSerializable(
381 | KEY,
382 | mLruCache.getSharedPreferences(name, keyAlias).getStringSet(
383 | key,
384 | emptySet()
385 | ) as? HashSet
386 | )
387 |
388 | GET_STRING -> bundle.putString(
389 | KEY,
390 | mLruCache.getSharedPreferences(name, keyAlias).getString(key, defValue)
391 | )
392 |
393 | GET_INT -> bundle.putInt(
394 | KEY,
395 | mLruCache.getSharedPreferences(name, keyAlias).getInt(key, defValue.toInt())
396 | )
397 |
398 | GET_LONG -> bundle.putLong(
399 | KEY,
400 | mLruCache.getSharedPreferences(name, keyAlias).getLong(key, defValue.toLong())
401 | )
402 |
403 | GET_FLOAT -> bundle.putFloat(
404 | KEY,
405 | mLruCache.getSharedPreferences(name, keyAlias).getFloat(key, defValue.toFloat())
406 | )
407 |
408 | GET_BOOLEAN -> bundle.putBoolean(
409 | KEY,
410 | mLruCache.getSharedPreferences(name, keyAlias)
411 | .getBoolean(key, java.lang.Boolean.parseBoolean(defValue))
412 | )
413 |
414 | CONTAINS -> bundle.putBoolean(
415 | KEY,
416 | mLruCache.getSharedPreferences(name, keyAlias).contains(key)
417 | )
418 |
419 | REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER -> {
420 | checkInitListenersCount()
421 | var countInteger = mListenersCount!![name]
422 | val count = (countInteger ?: 0) + 1
423 | mListenersCount!![name] = count
424 | countInteger = mListenersCount!![name]
425 | bundle.putBoolean(KEY, count == (countInteger ?: 0))
426 | }
427 |
428 | UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER -> {
429 | checkInitListenersCount()
430 | var countInteger = mListenersCount!![name]
431 | val count = (countInteger ?: 0) - 1
432 | if (count <= 0) {
433 | mListenersCount!!.remove(name)
434 | bundle.putBoolean(KEY, !mListenersCount!!.containsKey(name))
435 | } else {
436 | mListenersCount!![name] = count
437 | countInteger = mListenersCount!![name]
438 | bundle.putBoolean(KEY, count == (countInteger ?: 0))
439 | }
440 | }
441 |
442 | else -> throw IllegalArgumentException("This is Unknown Uri:$uri")
443 | }
444 | return BundleCursor(bundle)
445 | }
446 |
447 | override fun getType(uri: Uri): String? {
448 | throw UnsupportedOperationException("No external call")
449 | }
450 |
451 | override fun insert(uri: Uri, values: ContentValues?): Uri? {
452 | throw UnsupportedOperationException("No external insert")
453 | }
454 |
455 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
456 | throw UnsupportedOperationException("No external delete")
457 | }
458 |
459 | override fun update(
460 | uri: Uri,
461 | values: ContentValues?,
462 | selection: String?,
463 | selectionArgs: Array?
464 | ): Int {
465 | var result = 0
466 | val name = uri.pathSegments[0]
467 | val keyAlias = selectionArgs!![0]
468 | val preferences = mLruCache.getSharedPreferences(name, keyAlias)
469 | when (val match = mUriMatcher!!.match(uri)) {
470 | APPLY, COMMIT -> {
471 | val hasListeners =
472 | mListenersCount != null && mListenersCount!![name] != null && mListenersCount!![name]!! > 0
473 | var keysModified: ArrayList? = null
474 | var map: Map = HashMap()
475 | if (hasListeners) {
476 | keysModified = ArrayList()
477 | map = preferences.all as Map
478 | }
479 | val editor = preferences.edit()
480 | val clear = java.lang.Boolean.parseBoolean(selectionArgs[1])
481 | if (clear) {
482 | if (hasListeners && map.isNotEmpty()) {
483 | for ((key) in map) {
484 | keysModified!!.add(key)
485 | }
486 | }
487 | editor.clear()
488 | }
489 | for ((k, v) in values!!.valueSet()) {
490 | // Android 5.L_preview : "this" is the magic value for a removal mutation. In addition,
491 | // setting a value to "null" for a given key is specified to be
492 | // equivalent to calling remove on that key.
493 | if (v is EditorImpl || v == null) {
494 | editor.remove(k)
495 | if (hasListeners && map.containsKey(k)) {
496 | keysModified!!.add(k)
497 | }
498 | } else {
499 | if (hasListeners && (!map.containsKey(k) || (map.containsKey(k) && v != map[k]))) {
500 | keysModified!!.add(k)
501 | }
502 | }
503 | when (v) {
504 | is String -> {
505 | editor.putString(k, v)
506 | }
507 |
508 | is Set<*> -> {
509 | editor.putStringSet(
510 | k, v as Set
511 | )
512 | }
513 |
514 | is Int -> {
515 | editor.putInt(k, v)
516 | }
517 |
518 | is Long -> {
519 | editor.putLong(k, v)
520 | }
521 |
522 | is Float -> {
523 | editor.putFloat(k, v)
524 | }
525 |
526 | is Boolean -> {
527 | editor.putBoolean(k, v)
528 | }
529 | }
530 | }
531 | if (hasListeners && keysModified!!.isEmpty()) {
532 | result = 1
533 | } else {
534 | when (match) {
535 | APPLY -> {
536 | editor.apply()
537 | result = 1
538 | // Okay to notify the listeners before it's hit disk
539 | // because the listeners should always get the same
540 | // SharedPreferences instance back, which has the
541 | // changes reflected in memory.
542 | notifyListeners(name, keysModified)
543 | }
544 |
545 | COMMIT -> if (editor.commit()) {
546 | result = 1
547 | notifyListeners(name, keysModified)
548 | }
549 | }
550 | }
551 | values.clear()
552 | }
553 |
554 | else -> throw IllegalArgumentException("This is Unknown Uri:$uri")
555 | }
556 | return result
557 | }
558 |
559 | override fun onLowMemory() {
560 | if (mListenersCount != null) {
561 | mListenersCount!!.clear()
562 | }
563 | super.onLowMemory()
564 | }
565 |
566 | override fun onTrimMemory(level: Int) {
567 | if (mListenersCount != null) {
568 | mListenersCount!!.clear()
569 | }
570 | super.onTrimMemory(level)
571 | }
572 |
573 | private fun checkInitListenersCount() {
574 | if (mListenersCount == null) {
575 | mListenersCount = HashMap()
576 | }
577 | }
578 |
579 | private fun notifyListeners(name: String, keysModified: ArrayList?) {
580 | if (!keysModified.isNullOrEmpty()) {
581 | val intent = Intent()
582 | intent.action = makeAction(name)
583 | intent.setPackage(context!!.packageName)
584 | intent.putExtra(KEY_NAME, name)
585 | intent.putExtra(KEY, keysModified)
586 | context!!.sendBroadcast(intent)
587 | }
588 | }
589 |
590 | private class BundleCursor(private var mBundle: Bundle) : MatrixCursor(arrayOf(), 0) {
591 | override fun getExtras(): Bundle {
592 | return mBundle
593 | }
594 |
595 | override fun respond(extras: Bundle): Bundle {
596 | mBundle = extras
597 | return mBundle
598 | }
599 | }
600 |
601 | companion object {
602 | private const val TAG = "MicroMsg.MultiProcessSharedPreferences"
603 | const val DEBUG = false
604 | private var AUTHORITY: String? = null
605 |
606 | @Volatile
607 | private var AUTHORITY_URI: Uri? = null
608 | private const val KEY = "value"
609 | private const val KEY_NAME = "name"
610 | private const val PATH_WILDCARD = "*/"
611 | private const val PATH_GET_ALL = "getAll"
612 | private const val PATH_GET_SET_STRING = "getStringSet"
613 | private const val PATH_GET_STRING = "getString"
614 | private const val PATH_GET_INT = "getInt"
615 | private const val PATH_GET_LONG = "getLong"
616 | private const val PATH_GET_FLOAT = "getFloat"
617 | private const val PATH_GET_BOOLEAN = "getBoolean"
618 | private const val PATH_CONTAINS = "contains"
619 | private const val PATH_APPLY = "apply"
620 | private const val PATH_COMMIT = "commit"
621 | private const val PATH_REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER =
622 | "registerOnSharedPreferenceChangeListener"
623 | private const val PATH_UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER =
624 | "unregisterOnSharedPreferenceChangeListener"
625 | private const val GET_ALL = 0
626 | private const val GET_SET_STRING = 1
627 | private const val GET_STRING = 2
628 | private const val GET_INT = 3
629 | private const val GET_LONG = 4
630 | private const val GET_FLOAT = 5
631 | private const val GET_BOOLEAN = 6
632 | private const val CONTAINS = 7
633 | private const val APPLY = 8
634 | private const val COMMIT = 9
635 | private const val REGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = 10
636 | private const val UNREGISTER_ON_SHARED_PREFERENCE_CHANGE_LISTENER = 11
637 | private fun queryAuthority(context: Context?): String? {
638 | val packageInfo = try {
639 | context?.packageManager?.getPackageInfo(
640 | context.packageName,
641 | PackageManager.GET_PROVIDERS
642 | )
643 | } catch (e: PackageManager.NameNotFoundException) {
644 | // not required
645 | null
646 | }
647 | if (packageInfo?.providers != null) {
648 | for (providerInfo in packageInfo.providers) {
649 | if (providerInfo.name == MultiProcessSharedPreferences::class.java.name) {
650 | return providerInfo.authority
651 | }
652 | }
653 | }
654 | return null
655 | }
656 |
657 | fun getSharedPreferences(
658 | context: Context,
659 | name: String,
660 | keyAlias: String?
661 | ): SharedPreferences {
662 | return MultiProcessSharedPreferences(context, name, keyAlias)
663 | }
664 | }
665 | }
--------------------------------------------------------------------------------
/preferences-core/src/main/java/com/forjrking/preferences/provide/ProvideKt.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences.provide
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import android.os.Build
7 | import android.security.keystore.KeyGenParameterSpec
8 | import android.security.keystore.KeyProperties
9 | import android.util.Log
10 | import java.util.*
11 | import java.util.concurrent.ConcurrentLinkedQueue
12 |
13 |
14 | /**
15 | * DES: 返回hook的SharedPreferences实例
16 | * 8.0以下 Reflect pending work finishers
17 | * 8.0以上 Reflect finishers PrivateApi
18 | * @author: 岛主
19 | * @date: 2020/7/21 11:21
20 | * @version: 1.0.0
21 | * 生成支持多进程的mmkv
22 | * @param name xml名称 默认包名,建议给名字否则出现操作同key问题
23 | * @param cryptKey 加密密钥 mmkv加密密钥 SharedPreferences 内部方法不支持加密
24 | * @param isMMKV 是否使用mmkv
25 | * @param isMultiProcess 是否使用多进程 建议mmkv搭配使用 sp性能很差
26 | *
27 | * 此方法不提供MMKV初始化需要自己操作配置
28 | */
29 | @JvmOverloads
30 | @SuppressLint("PrivateApi")
31 | @Suppress("UNCHECKED_CAST")
32 | internal fun Context.createSharedPreferences(
33 | name: String? = null,
34 | cryptKey: String? = null,
35 | isMultiProcess: Boolean = false,
36 | isMMKV: Boolean = false
37 | ): SharedPreferences {
38 | val xmlName = "${if (name.isNullOrEmpty()) packageName else name}_kv"
39 | return if (isMMKV) {
40 | if (com.tencent.mmkv.MMKV.getRootDir().isNullOrEmpty()) {
41 | Log.e("MMKV", "You forgot to initialize MMKV")
42 | com.tencent.mmkv.MMKV.initialize(this)
43 | }
44 | val mode = if (isMultiProcess) com.tencent.mmkv.MMKV.MULTI_PROCESS_MODE
45 | else com.tencent.mmkv.MMKV.SINGLE_PROCESS_MODE
46 | com.tencent.mmkv.MMKV.mmkvWithID(xmlName, mode, cryptKey)
47 | } else {
48 | run {
49 | try {
50 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
51 | val clz = Class.forName("android.app.QueuedWork")
52 | val field = clz.getDeclaredField("sPendingWorkFinishers")
53 | field.isAccessible = true
54 | val queue = field.get(clz) as? ConcurrentLinkedQueue
55 | if (queue != null) {
56 | val proxy = ConcurrentLinkedQueueProxy(queue)
57 | field.set(queue, proxy)
58 | }
59 | } else {
60 | val clz = Class.forName("android.app.QueuedWork")
61 | val field = clz.getDeclaredField("sFinishers")
62 | field.isAccessible = true
63 | val queue = field.get(clz) as? LinkedList
64 | if (queue != null) {
65 | val linkedListProxy = LinkedListProxy(queue)
66 | field.set(queue, linkedListProxy)
67 | }
68 | }
69 | } catch (e: Exception) {
70 | e.printStackTrace()
71 | }
72 | return if (isMultiProcess) {
73 | MultiProcessSharedPreferences.getSharedPreferences(this, xmlName, cryptKey)
74 | } else {
75 | compatSharedPreferences(this, xmlName, cryptKey)
76 | }
77 | }
78 | }
79 | }
80 |
81 | internal fun compatSharedPreferences(
82 | context: Context,
83 | name: String,
84 | keyAlias: String?
85 | ): SharedPreferences {
86 | return if (keyAlias.isNullOrEmpty()) {
87 | context.getSharedPreferences(name, Context.MODE_PRIVATE)
88 | } else {
89 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
90 | val buildAES256GCMKeyGenParameterSpec = KeyGenParameterSpec.Builder(
91 | keyAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
92 | ).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
93 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
94 | .setKeySize(256)
95 | .build()
96 | androidx.security.crypto.EncryptedSharedPreferences.create(
97 | name,
98 | androidx.security.crypto.MasterKeys.getOrCreate(buildAES256GCMKeyGenParameterSpec),
99 | context,
100 | androidx.security.crypto.EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
101 | androidx.security.crypto.EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
102 | )
103 | } else {
104 | TODO("crypto must >= M")
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/preferences-core/src/main/java/com/forjrking/preferences/provide/QueuedWork.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences.provide
2 |
3 | import java.util.*
4 | import java.util.concurrent.ConcurrentLinkedQueue
5 |
6 | /**
7 | * 在8.0以下代理
8 | * The set of Runnables that will finish or wait on any async activities started by the application.
9 | * private static final ConcurrentLinkedQueue sPendingWorkFinishers = new ConcurrentLinkedQueue();
10 | */
11 |
12 | internal class ConcurrentLinkedQueueProxy(private val sPendingWorkFinishers: ConcurrentLinkedQueue) :
13 | ConcurrentLinkedQueue() {
14 |
15 | override fun add(element: Runnable?): Boolean = sPendingWorkFinishers.add(element)
16 |
17 | override fun remove(element: Runnable?): Boolean = sPendingWorkFinishers.remove(element)
18 |
19 | override fun isEmpty(): Boolean = true
20 |
21 | /**
22 | * 代理的poll()方法,永远返回空,这样UI线程就可以避免被阻塞,继续执行了
23 | */
24 | override fun poll(): Runnable? = null
25 | }
26 |
27 | /**
28 | * 在8.0以上apply()中QueuedWork.addFinisher(awaitCommit), 需要代理的是LinkedList,如下:
29 | * # private static final LinkedList sFinishers = new LinkedList<>()
30 | */
31 | internal class LinkedListProxy(private val sFinishers: LinkedList) :
32 | LinkedList() {
33 |
34 | override fun add(element: Runnable): Boolean = sFinishers.add(element)
35 |
36 | override fun remove(element: Runnable): Boolean = sFinishers.remove(element)
37 |
38 | override fun isEmpty(): Boolean = true
39 |
40 | /**
41 | * 代理的poll()方法,永远返回空,这样UI线程就可以避免被阻塞,继续执行了
42 | */
43 | override fun poll(): Runnable? = null
44 | }
45 |
--------------------------------------------------------------------------------
/preferences-core/src/main/java/com/forjrking/preferences/serialize/Serializer.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences.serialize
2 |
3 | import java.lang.reflect.Type
4 |
5 | interface Serializer {
6 |
7 | fun serialize(toSerialize: Any?): String?
8 |
9 | fun deserialize(serialized: String?, type: Type): Any?
10 | }
11 |
--------------------------------------------------------------------------------
/preferences-core/src/test/java/com/forjrking/preferences/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences
2 |
3 | import org.junit.jupiter.api.Assertions.assertNotNull
4 | import org.junit.jupiter.api.Test
5 | import kotlin.reflect.KClass
6 | import kotlin.reflect.javaType
7 | import kotlin.reflect.typeOf
8 | import kotlin.system.measureTimeMillis
9 |
10 | /**
11 | * Example local unit test, which will execute on the development machine (host).
12 | *
13 | * See [testing documentation](http://d.android.com/tools/testing).
14 | */
15 | val KClass<*>.isBasic
16 | get() = when (this) {
17 | Int::class, Float::class, Long::class, Boolean::class, String::class -> true
18 | else -> false
19 | }
20 |
21 | /**
22 | * sp需要序列化时候传递 type
23 | */
24 | @OptIn(ExperimentalStdlibApi::class)
25 | inline fun type() =
26 | if (T::class.isBasic) typeOf().javaType else typeOf().javaType
27 |
28 | class ExampleUnitTest {
29 | @OptIn(ExperimentalStdlibApi::class)
30 | @Test
31 | fun addition_isCorrect() {
32 |
33 | val writeTimeMillis3 = measureTimeMillis {
34 | repeat(1000) {
35 | assertNotNull(typeOf>().javaType)
36 | }
37 | }
38 | println("Time3: $writeTimeMillis3")
39 | }
40 | }
--------------------------------------------------------------------------------
/preferences-core/src/test/java/com/forjrking/preferences/cache/AtomicCacheTest.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences.cache
2 |
3 | import org.junit.jupiter.api.Assertions.assertEquals
4 | import org.junit.jupiter.api.Assertions.assertTrue
5 | import org.junit.jupiter.api.BeforeEach
6 | import org.junit.jupiter.api.Test
7 |
8 |
9 | /**
10 | * @description:
11 | * @author: forjrking
12 | * @date: 2023/5/21 20:48
13 | */
14 | internal class AtomicCacheTest {
15 |
16 | private var flag: Int = 0
17 |
18 | @BeforeEach
19 | fun setUp() {
20 | flag = 0
21 | }
22 |
23 | private fun increase() {
24 | flag++
25 | }
26 |
27 | @Test
28 | fun `when double incept string then caching`() {
29 | val atomicCache = AtomicCache(true)
30 | val value = "abc"
31 | atomicCache.incept(value, ::increase)
32 | assertEquals(1, flag)
33 | atomicCache.incept(value, ::increase)
34 | assertEquals(1, flag)
35 | }
36 |
37 | @Test
38 | fun `when double incept List then no caching`() {
39 | val atomicCache = AtomicCache>(true)
40 | val value = mutableListOf("abc")
41 | atomicCache.incept(value, ::increase)
42 | assertEquals(value, atomicCache.acquire { emptyList() })
43 | assertEquals(1, flag)
44 | atomicCache.incept(listOf("abc"), ::increase)
45 | assertEquals(1, flag)
46 | atomicCache.incept(value.apply { add("def") }, ::increase)
47 | assertEquals(value, atomicCache.acquire { emptyList() })
48 | assertEquals(2, flag)
49 | }
50 |
51 | @Test
52 | fun `when double incept Data class then no caching`() {
53 | data class TestBo(var name: String, val list: MutableList)
54 |
55 | val atomicCache = AtomicCache(true)
56 | val value = TestBo("abc", mutableListOf("abc"))
57 | atomicCache.incept(value, ::increase)
58 | assertEquals(1, flag)
59 | atomicCache.incept(TestBo("abc", mutableListOf("abc")), ::increase)
60 | assertEquals(1, flag)
61 | atomicCache.incept(value.apply {
62 | name = ("def")
63 | list.add("def")
64 | }, ::increase)
65 | assertEquals(2, flag)
66 | }
67 |
68 | @Test
69 | fun sameCaching() {
70 | val atomicCache = AtomicCache(true)
71 | atomicCache.incept(null, ::increase)
72 | assertTrue(atomicCache.sameCaching(null, null))
73 | }
74 | }
--------------------------------------------------------------------------------
/preferences-core/src/test/java/com/forjrking/preferences/extensions/EditorExtKtKtTest.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences.extensions
2 |
3 | import android.content.SharedPreferences
4 | import org.junit.jupiter.api.Assertions.assertEquals
5 | import org.junit.jupiter.api.Assertions.assertThrows
6 | import org.junit.jupiter.api.BeforeEach
7 | import org.junit.jupiter.api.Test
8 | import org.mockito.kotlin.anyOrNull
9 | import org.mockito.kotlin.doReturn
10 | import org.mockito.kotlin.eq
11 | import org.mockito.kotlin.mock
12 | import kotlin.reflect.jvm.javaType
13 | import kotlin.reflect.typeOf
14 |
15 | /**
16 | * @description:
17 | * @author: forjrking
18 | * @date: 2023/5/22 15:46
19 | */
20 | internal class EditorExtKtKtTest {
21 |
22 | private val UNIT_TYPE = typeOf().javaType
23 |
24 | private lateinit var mockSP: SharedPreferences
25 |
26 | private val key = "key"
27 |
28 | @BeforeEach
29 | fun setUp() {
30 | mockSP = mock() {
31 | val mockEditor = mock()
32 | on { edit() } doReturn mockEditor
33 | on { getInt(eq(key), anyOrNull()) } doReturn 1
34 | on { getFloat(eq(key), anyOrNull()) } doReturn 1F
35 | on { getLong(eq(key), anyOrNull()) } doReturn 1L
36 | on { getBoolean(eq(key), anyOrNull()) } doReturn true
37 | on { getString(eq(key), anyOrNull()) } doReturn "ABC"
38 | on { getStringSet(eq(key), anyOrNull()) } doReturn null
39 | }
40 | }
41 |
42 |
43 | @Test
44 | fun putValue() {
45 | mockSP.edit().putValue(Int::class, UNIT_TYPE, key, 0)
46 | mockSP.edit().putValue(Float::class, UNIT_TYPE, key, 1F)
47 | mockSP.edit().putValue(Long::class, UNIT_TYPE, key, 2L)
48 | }
49 |
50 | @Test
51 | fun getValue() {
52 | var value = mockSP.getValue(Int::class, UNIT_TYPE, key, 0)
53 | assertEquals(1, value)
54 | value = mockSP.getValue(Float::class, UNIT_TYPE, key, 0F)
55 | assertEquals(1F, value)
56 | value = mockSP.getValue(Long::class, UNIT_TYPE, key, 0L)
57 | assertEquals(1L, value)
58 | value = mockSP.getValue(String::class, UNIT_TYPE, key, null)
59 | assertEquals("ABC", value)
60 | }
61 |
62 | @Test
63 | fun getValueObj() {
64 | val value = mockSP.getValue(Set::class, typeOf>().javaType, key, null)
65 | assertEquals(null, value)
66 | }
67 |
68 | @Test
69 | fun `getValueObj not support`() {
70 | assertThrows(IllegalStateException::class.java) {
71 | val value = mockSP.getValue(Set::class, typeOf>().javaType, key, null)
72 | assertEquals(null, value)
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/preferences-gson/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/preferences-gson/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'com.forjrking.preferences'
9 | compileSdk libs.versions.compileSdk.get().toInteger()
10 |
11 | defaultConfig {
12 | minSdkVersion libs.versions.minSdk.get().toInteger()
13 | targetSdkVersion libs.versions.targetSdk.get().toInteger()
14 | versionCode libs.versions.preference.versionCode.get().toInteger()
15 | versionName libs.versions.preference.version.get()
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | compileOptions {
20 | sourceCompatibility = JavaVersion.VERSION_1_8
21 | targetCompatibility = JavaVersion.VERSION_1_8
22 | }
23 | kotlinOptions {
24 | jvmTarget = '1.8'
25 | freeCompilerArgs += ['-module-name', "preferences"]
26 | }
27 | buildTypes {
28 | release {
29 | minifyEnabled false
30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
31 | }
32 | }
33 | publishing {
34 | singleVariant("release") {
35 | // if you don't want sources/javadoc, remove these lines
36 | withSourcesJar()
37 | withJavadocJar()
38 | }
39 | }
40 | }
41 |
42 | dependencies {
43 | compileOnly project(":preferences-core")
44 | implementation libs.gson
45 | implementation libs.androidx.startup.runtime
46 | testImplementation libs.junit5
47 | testRuntimeOnly libs.junit.engine
48 | }
49 |
50 | afterEvaluate {
51 | publishing {
52 | publications {
53 | // Creates a Maven publication called "release".
54 | release(MavenPublication) {
55 | // Applies the component for the release build variant.
56 | from components.release
57 | // You can then customize attributes of the publication as shown below.
58 | groupId = 'com.github.forjrking.pref'
59 | artifactId = 'pref-gson'
60 | version = android.defaultConfig.versionName
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/preferences-gson/consumer-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class com.forjrking.preferences.serialize.GsonSerializerInitializer {*;}
--------------------------------------------------------------------------------
/preferences-gson/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/preferences-gson/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/preferences-gson/src/main/java/com/forjrking/preferences/serialize/GsonSerializer.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences.serialize
2 |
3 | import android.content.Context
4 | import androidx.startup.Initializer
5 | import com.forjrking.preferences.PreferencesOwner
6 | import com.google.gson.Gson
7 | import com.google.gson.GsonBuilder
8 | import java.lang.reflect.Type
9 |
10 | class GsonSerializerInitializer : Initializer {
11 |
12 | private val gson by lazy { GsonBuilder().serializeNulls().create() }
13 |
14 | override fun create(context: Context) {
15 | PreferencesOwner.context = context.applicationContext
16 | PreferencesOwner.serializer = GsonSerializer(gson)
17 | }
18 |
19 | override fun dependencies() = emptyList>>()
20 | }
21 |
22 | class GsonSerializer(private val gson: Gson) : Serializer {
23 |
24 | override fun serialize(toSerialize: Any?): String? = gson.toJson(toSerialize)
25 |
26 | override fun deserialize(serialized: String?, type: Type): Any? = try {
27 | gson.fromJson(serialized, type)
28 | } catch (e: Throwable) {
29 | null
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/preferences-gson/src/test/java/com/forjrking/preferences/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences
2 |
3 | import org.junit.jupiter.api.Assertions.assertEquals
4 | import org.junit.jupiter.api.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
--------------------------------------------------------------------------------
/preferences-ktx/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/preferences-ktx/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'com.forjrking.preferences'
9 | compileSdk libs.versions.compileSdk.get().toInteger()
10 |
11 | defaultConfig {
12 | minSdkVersion libs.versions.minSdk.get().toInteger()
13 | targetSdkVersion libs.versions.targetSdk.get().toInteger()
14 | versionCode libs.versions.preference.versionCode.get().toInteger()
15 | versionName libs.versions.preference.version.get()
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | compileOptions {
20 | sourceCompatibility = JavaVersion.VERSION_1_8
21 | targetCompatibility = JavaVersion.VERSION_1_8
22 | }
23 | kotlinOptions {
24 | jvmTarget = '1.8'
25 | freeCompilerArgs += ['-module-name', "preferences"]
26 | }
27 | buildTypes {
28 | release {
29 | minifyEnabled false
30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
31 | }
32 | }
33 | publishing {
34 | singleVariant("release") {
35 | // if you don't want sources/javadoc, remove these lines
36 | withSourcesJar()
37 | withJavadocJar()
38 | }
39 | }
40 | }
41 |
42 | dependencies {
43 | compileOnly project(":preferences-core")
44 | implementation libs.androidx.lifecycle.livedata
45 | testImplementation libs.junit5
46 | testRuntimeOnly libs.junit.engine
47 | }
48 |
49 | afterEvaluate {
50 | publishing {
51 | publications {
52 | // Creates a Maven publication called "release".
53 | release(MavenPublication) {
54 | // Applies the component for the release build variant.
55 | from components.release
56 | // You can then customize attributes of the publication as shown below.
57 | groupId = 'com.github.forjrking.pref'
58 | artifactId = 'pref-ktx'
59 | version = android.defaultConfig.versionName
60 | }
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/preferences-ktx/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/forJrking/Preferences/287daffa950b01074a4fae9f6e1ceb1857a9ca06/preferences-ktx/consumer-rules.pro
--------------------------------------------------------------------------------
/preferences-ktx/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/preferences-ktx/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/preferences-ktx/src/main/java/com/forjrking/preferences/ktx/PreferencesKt.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences.ktx
2 |
3 | import androidx.lifecycle.MutableLiveData
4 | import com.forjrking.preferences.PreferencesOwner
5 | import kotlin.properties.ReadOnlyProperty
6 | import kotlin.properties.ReadWriteProperty
7 | import kotlin.reflect.KProperty
8 |
9 | fun ReadWriteProperty.asLiveData() =
10 | object : ReadOnlyProperty> {
11 | private var cache: MutableLiveData? = null
12 |
13 | override fun getValue(thisRef: PreferencesOwner, property: KProperty<*>) =
14 | cache ?: object : MutableLiveData() {
15 | override fun getValue() = this@asLiveData.getValue(thisRef, property)
16 |
17 | override fun setValue(value: V) {
18 | this@asLiveData.setValue(thisRef, property, value)
19 | super.setValue(value)
20 | }
21 |
22 | override fun onActive() = super.setValue(value)
23 | }.also { cache = it }
24 | }
25 |
--------------------------------------------------------------------------------
/preferences-ktx/src/test/java/com/forjrking/preferences/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.forjrking.preferences
2 |
3 | import org.junit.jupiter.api.Assertions.assertEquals
4 | import org.junit.jupiter.api.Test
5 |
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | maven { url = java.net.URI("https://jitpack.io") }
15 | }
16 | }
17 |
18 | include(":app")
19 | include(":preferences-core")
20 | include(":preferences-gson")
21 | include(":preferences-ktx")
22 |
--------------------------------------------------------------------------------