1 /*
<lambda>null2  * Copyright 2019 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 package androidx.navigation.dynamicfeatures
18 
19 import android.content.Context
20 import android.os.Bundle
21 import android.util.Log
22 import androidx.annotation.RestrictTo
23 import androidx.lifecycle.MutableLiveData
24 import androidx.navigation.NavBackStackEntry
25 import androidx.navigation.NavDestination
26 import androidx.navigation.Navigator
27 import androidx.navigation.dynamicfeatures.DynamicGraphNavigator.DynamicNavGraph
28 import androidx.navigation.get
29 import com.google.android.play.core.splitcompat.SplitCompat
30 import com.google.android.play.core.splitinstall.SplitInstallException
31 import com.google.android.play.core.splitinstall.SplitInstallHelper
32 import com.google.android.play.core.splitinstall.SplitInstallManager
33 import com.google.android.play.core.splitinstall.SplitInstallRequest
34 import com.google.android.play.core.splitinstall.SplitInstallSessionState
35 import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
36 import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode
37 import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
38 
39 /**
40  * Install manager for dynamic features.
41  *
42  * Enables installation of dynamic features for both installed and instant apps.
43  */
44 public open class DynamicInstallManager(
45     private val context: Context,
46     private val splitInstallManager: SplitInstallManager
47 ) {
48 
49     internal companion object {
50         internal fun terminateLiveData(status: MutableLiveData<SplitInstallSessionState>) {
51             // Best effort leak prevention, will only work for active observers
52             check(!status.hasActiveObservers()) {
53                 "This DynamicInstallMonitor will not " +
54                     "emit any more status updates. You should remove all " +
55                     "Observers after null has been emitted."
56             }
57         }
58     }
59 
60     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
61     public fun performInstall(
62         backStackEntry: NavBackStackEntry,
63         extras: DynamicExtras?,
64         moduleName: String
65     ): NavDestination? {
66         if (extras?.installMonitor != null) {
67             requestInstall(moduleName, extras.installMonitor)
68             return null
69         } else {
70             val progressArgs =
71                 Bundle().apply {
72                     putInt(Constants.DESTINATION_ID, backStackEntry.destination.id)
73                     putBundle(Constants.DESTINATION_ARGS, backStackEntry.arguments)
74                 }
75             val dynamicNavGraph = DynamicNavGraph.getOrThrow(backStackEntry.destination)
76             val navigator: Navigator<*> =
77                 dynamicNavGraph.navigatorProvider[dynamicNavGraph.navigatorName]
78             if (navigator is DynamicGraphNavigator) {
79                 navigator.navigateToProgressDestination(dynamicNavGraph, progressArgs)
80                 return null
81             } else {
82                 throw IllegalStateException(
83                     "You must use a DynamicNavGraph to perform a module installation."
84                 )
85             }
86         }
87     }
88 
89     /**
90      * @param module The module to install.
91      * @return Whether the requested module needs installation.
92      */
93     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
94     public fun needsInstall(module: String): Boolean {
95         return !splitInstallManager.installedModules.contains(module)
96     }
97 
98     private fun requestInstall(module: String, installMonitor: DynamicInstallMonitor) {
99         check(!installMonitor.isUsed) {
100             // We don't want an installMonitor in an undefined state or used by another install
101             "You must pass in a fresh DynamicInstallMonitor " +
102                 "in DynamicExtras every time you call navigate()."
103         }
104 
105         val status = installMonitor.status as MutableLiveData<SplitInstallSessionState>
106         installMonitor.isInstallRequired = true
107 
108         val request = SplitInstallRequest.newBuilder().addModule(module).build()
109 
110         splitInstallManager
111             .startInstall(request)
112             .addOnSuccessListener { sessionId ->
113                 installMonitor.sessionId = sessionId
114                 installMonitor.splitInstallManager = splitInstallManager
115                 if (sessionId == 0) {
116                     // The feature is already installed, emit synthetic INSTALLED state.
117                     status.value =
118                         SplitInstallSessionState.create(
119                             sessionId,
120                             SplitInstallSessionStatus.INSTALLED,
121                             SplitInstallErrorCode.NO_ERROR,
122                             /* bytesDownloaded */ 0,
123                             /* totalBytesToDownload */ 0,
124                             listOf(module),
125                             emptyList()
126                         )
127                     terminateLiveData(status)
128                 } else {
129                     val listener = SplitInstallListenerWrapper(context, status, installMonitor)
130                     splitInstallManager.registerListener(listener)
131                 }
132             }
133             .addOnFailureListener { exception ->
134                 Log.i(
135                     "DynamicInstallManager",
136                     "Error requesting install of $module: ${exception.message}"
137                 )
138                 installMonitor.exception = exception
139                 status.value =
140                     SplitInstallSessionState.create(
141                         /* sessionId */ 0,
142                         SplitInstallSessionStatus.FAILED,
143                         if (exception is SplitInstallException) exception.errorCode
144                         else SplitInstallErrorCode.INTERNAL_ERROR,
145                         /* bytesDownloaded */ 0,
146                         /* totalBytesToDownload */ 0,
147                         listOf(module),
148                         emptyList()
149                     )
150                 terminateLiveData(status)
151             }
152     }
153 
154     private class SplitInstallListenerWrapper(
155         private val context: Context,
156         private val status: MutableLiveData<SplitInstallSessionState>,
157         private val installMonitor: DynamicInstallMonitor
158     ) : SplitInstallStateUpdatedListener {
159 
160         override fun onStateUpdate(splitInstallSessionState: SplitInstallSessionState) {
161             if (splitInstallSessionState.sessionId() == installMonitor.sessionId) {
162                 if (splitInstallSessionState.status() == SplitInstallSessionStatus.INSTALLED) {
163                     SplitCompat.install(context)
164                     // Enable immediate usage of dynamic feature modules in an instant app context.
165                     SplitInstallHelper.updateAppInfo(context)
166                 }
167                 status.value = splitInstallSessionState
168                 if (splitInstallSessionState.hasTerminalStatus()) {
169                     installMonitor.splitInstallManager!!.unregisterListener(this)
170                     terminateLiveData(status)
171                 }
172             }
173         }
174     }
175 }
176