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