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