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.fragment.ui 18 19 import android.app.Activity 20 import android.content.IntentSender 21 import android.os.Bundle 22 import android.util.Log 23 import android.view.View 24 import androidx.activity.result.IntentSenderRequest 25 import androidx.activity.result.contract.ActivityResultContracts 26 import androidx.fragment.app.Fragment 27 import androidx.lifecycle.Observer 28 import androidx.lifecycle.ViewModelProvider 29 import androidx.navigation.dynamicfeatures.Constants 30 import androidx.navigation.dynamicfeatures.DynamicExtras 31 import androidx.navigation.dynamicfeatures.DynamicInstallMonitor 32 import androidx.navigation.fragment.findNavController 33 import com.google.android.play.core.common.IntentSenderForResultStarter 34 import com.google.android.play.core.splitinstall.SplitInstallSessionState 35 import com.google.android.play.core.splitinstall.model.SplitInstallErrorCode 36 import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus 37 38 /** 39 * The base class for [Fragment]s that handle dynamic feature installation. 40 * 41 * When extending from this class, you are responsible for forwarding installation state changes to 42 * your UI via the provided hooks in [onCancelled], [onFailed], [onProgress]. 43 * 44 * The installation process itself is handled within the [AbstractProgressFragment] itself. 45 * Navigation to the target destination will occur once the installation is completed. 46 */ 47 public abstract class AbstractProgressFragment : Fragment { 48 49 internal companion object { 50 private const val INSTALL_REQUEST_CODE = 1 51 private const val TAG = "AbstractProgress" 52 } 53 54 private val installViewModel: InstallViewModel by lazy { 55 ViewModelProvider(viewModelStore, InstallViewModel.FACTORY, defaultViewModelCreationExtras)[ 56 InstallViewModel::class.java] 57 } 58 private val destinationId by lazy { requireArguments().getInt(Constants.DESTINATION_ID) } 59 private val destinationArgs: Bundle? by lazy { 60 requireArguments().getBundle(Constants.DESTINATION_ARGS) 61 } 62 private var navigated = false 63 64 public constructor() 65 66 public constructor(contentLayoutId: Int) : super(contentLayoutId) 67 68 private val intentSenderLauncher = 69 registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result -> 70 if (result.resultCode == Activity.RESULT_CANCELED) { 71 onCancelled() 72 } 73 } 74 75 override fun onCreate(savedInstanceState: Bundle?) { 76 super.onCreate(savedInstanceState) 77 if (savedInstanceState != null) { 78 navigated = savedInstanceState.getBoolean(Constants.KEY_NAVIGATED, false) 79 } 80 } 81 82 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 83 if (navigated) { 84 findNavController().popBackStack() 85 return 86 } 87 var monitor = installViewModel.installMonitor 88 if (monitor == null) { 89 Log.i(TAG, "onViewCreated: monitor is null, navigating") 90 navigate() 91 monitor = installViewModel.installMonitor 92 } 93 if (monitor != null) { 94 Log.i(TAG, "onViewCreated: monitor is now not null, observing") 95 monitor.status.observe(viewLifecycleOwner, StateObserver(monitor)) 96 } 97 } 98 99 /** Navigates to an installed dynamic feature module or kicks off installation. */ 100 internal fun navigate() { 101 Log.i(TAG, "navigate: ") 102 val installMonitor = DynamicInstallMonitor() 103 val extras = DynamicExtras(installMonitor) 104 findNavController().navigate(destinationId, destinationArgs, null, extras) 105 if (!installMonitor.isInstallRequired) { 106 Log.i(TAG, "navigate: install not required") 107 navigated = true 108 } else { 109 Log.i(TAG, "navigate: setting install monitor") 110 installViewModel.installMonitor = installMonitor 111 } 112 } 113 114 override fun onSaveInstanceState(outState: Bundle) { 115 super.onSaveInstanceState(outState) 116 outState.putBoolean(Constants.KEY_NAVIGATED, navigated) 117 } 118 119 private inner class StateObserver constructor(private val monitor: DynamicInstallMonitor) : 120 Observer<SplitInstallSessionState> { 121 122 override fun onChanged( 123 @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") sessionState: SplitInstallSessionState 124 ) { 125 if (sessionState.hasTerminalStatus()) { 126 monitor.status.removeObserver(this) 127 } 128 when (sessionState.status()) { 129 SplitInstallSessionStatus.INSTALLED -> { 130 onInstalled() 131 navigate() 132 } 133 SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION -> 134 try { 135 val splitInstallManager = monitor.splitInstallManager 136 if (splitInstallManager == null) { 137 onFailed(SplitInstallErrorCode.INTERNAL_ERROR) 138 return 139 } 140 splitInstallManager.startConfirmationDialogForResult( 141 sessionState, 142 IntentSenderForResultStarter { 143 intent, 144 _, 145 fillInIntent, 146 flagsMask, 147 flagsValues, 148 _, 149 _ -> 150 intentSenderLauncher.launch( 151 IntentSenderRequest.Builder(intent) 152 .setFillInIntent(fillInIntent) 153 .setFlags(flagsValues, flagsMask) 154 .build() 155 ) 156 }, 157 INSTALL_REQUEST_CODE 158 ) 159 } catch (e: IntentSender.SendIntentException) { 160 onFailed(SplitInstallErrorCode.INTERNAL_ERROR) 161 } 162 SplitInstallSessionStatus.CANCELED -> onCancelled() 163 SplitInstallSessionStatus.FAILED -> onFailed(sessionState.errorCode()) 164 SplitInstallSessionStatus.UNKNOWN -> onFailed(SplitInstallErrorCode.INTERNAL_ERROR) 165 SplitInstallSessionStatus.CANCELING, 166 SplitInstallSessionStatus.DOWNLOADED, 167 SplitInstallSessionStatus.DOWNLOADING, 168 SplitInstallSessionStatus.INSTALLING, 169 SplitInstallSessionStatus.PENDING -> { 170 onProgress( 171 sessionState.status(), 172 sessionState.bytesDownloaded(), 173 sessionState.totalBytesToDownload() 174 ) 175 } 176 } 177 } 178 } 179 180 /** 181 * Called when there was a progress update for an active module download. 182 * 183 * @param status the current installation status from SplitInstallSessionStatus 184 * @param bytesDownloaded The bytes downloaded so far. 185 * @param bytesTotal The total bytes to be downloaded (can be 0 for some status updates) 186 */ 187 protected abstract fun onProgress( 188 @SplitInstallSessionStatus status: Int, 189 bytesDownloaded: Long, 190 bytesTotal: Long 191 ) 192 193 /** Called when the user decided to cancel installation. */ 194 protected abstract fun onCancelled() 195 196 /** 197 * Called when the installation has failed due to non-user issues. 198 * 199 * Please check [SplitInstallErrorCode] for error code constants. 200 * 201 * @param errorCode contains the error code of the installation failure. 202 */ 203 protected abstract fun onFailed(@SplitInstallErrorCode errorCode: Int) 204 205 /** 206 * Called when requested module has been successfully installed, just before the 207 * [NavController][androidx.navigation.NavController] navigates to the final destination. 208 */ 209 protected open fun onInstalled(): Unit = Unit 210 } 211