| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.navigation.dynamicfeatures |
| |
| import android.content.Context |
| import android.os.Bundle |
| import android.util.Log |
| import androidx.annotation.RestrictTo |
| import androidx.lifecycle.MutableLiveData |
| import androidx.navigation.NavDestination |
| import androidx.navigation.Navigator |
| import androidx.navigation.dynamicfeatures.DynamicGraphNavigator.DynamicNavGraph |
| import androidx.navigation.get |
| import com.google.android.play.core.splitcompat.SplitCompat |
| import com.google.android.play.core.splitinstall.SplitInstallException |
| import com.google.android.play.core.splitinstall.SplitInstallManager |
| import com.google.android.play.core.splitinstall.SplitInstallRequest |
| import com.google.android.play.core.splitinstall.SplitInstallSessionState |
| import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener |
| import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode |
| import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus |
| |
| /** |
| * Install manager for dynamic features. |
| * |
| * Enables installation of dynamic features. |
| */ |
| open class DynamicInstallManager( |
| private val context: Context, |
| private val splitInstallManager: SplitInstallManager |
| ) { |
| |
| internal companion object { |
| internal fun terminateLiveData( |
| status: MutableLiveData<SplitInstallSessionState> |
| ) { |
| // Best effort leak prevention, will only work for active observers |
| check(!status.hasActiveObservers()) { |
| "This DynamicInstallMonitor will not " + |
| "emit any more status updates. You should remove all " + |
| "Observers after null has been emitted." |
| } |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| fun performInstall( |
| destination: NavDestination, |
| args: Bundle?, |
| extras: DynamicExtras?, |
| moduleName: String |
| ): NavDestination? { |
| if (extras?.installMonitor != null) { |
| requestInstall(moduleName, extras.installMonitor) |
| return null |
| } else { |
| val progressArgs = Bundle().apply { |
| putInt(Constants.DESTINATION_ID, destination.id) |
| putBundle(Constants.DESTINATION_ARGS, args) |
| } |
| val dynamicNavGraph = DynamicNavGraph.getOrThrow(destination) |
| val navigator: Navigator<*> = |
| dynamicNavGraph.navigatorProvider[dynamicNavGraph.navigatorName] |
| return if (navigator is DynamicGraphNavigator) { |
| navigator.navigateToProgressDestination(dynamicNavGraph, progressArgs) |
| } else { |
| throw IllegalStateException( |
| "You must use a DynamicNavGraph to perform a module installation.") |
| } |
| } |
| } |
| |
| /** |
| * @param module The module to install. |
| * @return Whether the requested module needs installation. |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| fun needsInstall(module: String): Boolean { |
| return !splitInstallManager.installedModules.contains(module) |
| } |
| |
| private fun requestInstall( |
| module: String, |
| installMonitor: DynamicInstallMonitor |
| ) { |
| check(!installMonitor.isUsed) { |
| // We don't want an installMonitor in an undefined state or used by another install |
| "You must pass in a fresh DynamicInstallMonitor " + |
| "in DynamicExtras every time you call navigate()." |
| } |
| |
| val status = installMonitor.status as MutableLiveData<SplitInstallSessionState> |
| installMonitor.isInstallRequired = true |
| |
| val request = SplitInstallRequest |
| .newBuilder() |
| .addModule(module) |
| .build() |
| |
| splitInstallManager |
| .startInstall(request) |
| .addOnSuccessListener { sessionId -> |
| installMonitor.sessionId = sessionId |
| installMonitor.splitInstallManager = splitInstallManager |
| if (sessionId == 0) { |
| // The feature is already installed, emit synthetic INSTALLED state. |
| status.value = SplitInstallSessionState.create( |
| sessionId, |
| SplitInstallSessionStatus.INSTALLED, |
| SplitInstallErrorCode.NO_ERROR, |
| /* bytesDownloaded */ 0, |
| /* totalBytesToDownload */ 0, |
| listOf(module), |
| emptyList() |
| ) |
| terminateLiveData(status) |
| } else { |
| val listener = SplitInstallListenerWrapper( |
| context, status, |
| installMonitor |
| ) |
| splitInstallManager.registerListener(listener) |
| } |
| } |
| .addOnFailureListener { exception -> |
| Log.i("DynamicInstallManager", |
| "Error requesting install of $module: ${exception.message}") |
| installMonitor.exception = exception |
| status.value = SplitInstallSessionState.create( |
| /* sessionId */ 0, |
| SplitInstallSessionStatus.FAILED, |
| if (exception is SplitInstallException) |
| exception.errorCode |
| else |
| SplitInstallErrorCode.INTERNAL_ERROR, |
| /* bytesDownloaded */ 0, |
| /* totalBytesToDownload */ 0, |
| listOf(module), |
| emptyList() |
| ) |
| terminateLiveData(status) |
| } |
| } |
| |
| private class SplitInstallListenerWrapper internal constructor( |
| private val context: Context, |
| private val status: MutableLiveData<SplitInstallSessionState>, |
| private val installMonitor: DynamicInstallMonitor |
| ) : SplitInstallStateUpdatedListener { |
| |
| override fun onStateUpdate( |
| splitInstallSessionState: SplitInstallSessionState |
| ) { |
| if (splitInstallSessionState.sessionId() == installMonitor.sessionId) { |
| if (splitInstallSessionState.status() == SplitInstallSessionStatus.INSTALLED) { |
| SplitCompat.install(context) |
| } |
| status.value = splitInstallSessionState |
| if (splitInstallSessionState.hasTerminalStatus()) { |
| installMonitor.splitInstallManager!!.unregisterListener(this) |
| terminateLiveData(status) |
| } |
| } |
| } |
| } |
| } |