1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:OptIn(ExperimentalStdlibApi::class)
18 
19 package androidx.lifecycle.viewmodel.internal
20 
21 import androidx.annotation.MainThread
22 import androidx.lifecycle.ViewModel
23 import kotlin.concurrent.Volatile
24 import kotlinx.coroutines.CoroutineScope
25 
26 /**
27  * Internal implementation of the multiplatform [ViewModel].
28  *
29  * Kotlin Multiplatform does not support expect class with default implementation yet, so we
30  * extracted the common logic used by all platforms to this internal class.
31  *
32  * @see <a href="https://youtrack.jetbrains.com/issue/KT-20427">KT-20427</a>
33  */
34 internal class ViewModelImpl {
35 
36     private val lock = SynchronizedObject()
37 
38     /**
39      * Holds a mapping between [String] keys and [AutoCloseable] resources that have been associated
40      * with this [ViewModel].
41      *
42      * The associated resources will be [AutoCloseable.close] right before the [ViewModel.onCleared]
43      * is called. This provides automatic resource cleanup upon [ViewModel] release.
44      *
45      * For specifics about the clearing sequence, refer to the [ViewModel.clear] method.
46      *
47      * **Note:** Manually [SynchronizedObject] is necessary to prevent issues on Android API 21
48      * and 22. This avoids potential problems found in older versions of `ConcurrentHashMap`.
49      *
50      * @see <a href="https://issuetracker.google.com/37042460">b/37042460</a>
51      */
52     private val keyToCloseables = mutableMapOf<String, AutoCloseable>()
53 
54     /** @see [keyToCloseables] */
55     private val closeables = mutableSetOf<AutoCloseable>()
56 
57     @Volatile private var isCleared = false
58 
59     constructor()
60 
61     constructor(viewModelScope: CoroutineScope) {
62         addCloseable(VIEW_MODEL_SCOPE_KEY, viewModelScope.asCloseable())
63     }
64 
65     constructor(vararg closeables: AutoCloseable) {
66         this.closeables += closeables
67     }
68 
69     constructor(viewModelScope: CoroutineScope, vararg closeables: AutoCloseable) {
70         addCloseable(VIEW_MODEL_SCOPE_KEY, viewModelScope.asCloseable())
71         this.closeables += closeables
72     }
73 
74     /** @see [ViewModel.clear] */
75     @MainThread
clearnull76     fun clear() {
77         if (isCleared) return
78 
79         isCleared = true
80         synchronized(lock) {
81             for (closeable in keyToCloseables.values) {
82                 closeWithRuntimeException(closeable)
83             }
84             for (closeable in closeables) {
85                 closeWithRuntimeException(closeable)
86             }
87             // Clear only resources without keys to prevent accidental recreation of resources.
88             // For example, `viewModelScope` would be recreated leading to unexpected behaviour.
89             closeables.clear()
90         }
91     }
92 
93     /** @see [ViewModel.addCloseable] */
addCloseablenull94     fun addCloseable(key: String, closeable: AutoCloseable) {
95         // Although no logic should be done after user calls onCleared(), we will
96         // ensure that if it has already been called, the closeable attempting to
97         // be added will be closed immediately to ensure there will be no leaks.
98         if (isCleared) {
99             closeWithRuntimeException(closeable)
100             return
101         }
102 
103         val oldCloseable = synchronized(lock) { keyToCloseables.put(key, closeable) }
104         closeWithRuntimeException(oldCloseable)
105     }
106 
107     /** @see [ViewModel.addCloseable] */
addCloseablenull108     fun addCloseable(closeable: AutoCloseable) {
109         // Although no logic should be done after user calls onCleared(), we will
110         // ensure that if it has already been called, the closeable attempting to
111         // be added will be closed immediately to ensure there will be no leaks.
112         if (isCleared) {
113             closeWithRuntimeException(closeable)
114             return
115         }
116 
117         synchronized(lock) { closeables += closeable }
118     }
119 
120     /** @see [ViewModel.getCloseable] */
getCloseablenull121     fun <T : AutoCloseable> getCloseable(key: String): T? =
122         @Suppress("UNCHECKED_CAST") synchronized(lock) { keyToCloseables[key] as T? }
123 
closeWithRuntimeExceptionnull124     private fun closeWithRuntimeException(closeable: AutoCloseable?) {
125         try {
126             closeable?.close()
127         } catch (e: Exception) {
128             throw RuntimeException(e)
129         }
130     }
131 }
132