I am trying to install an app from my fragment. I want the unknown source permission message to be displayed and then the installation process to take place. The problem is that on my first installation, app seems to be crashed.
Of course, next time (when installing another apk) this problem disappears. I did the following steps:
In my viewModel :
fun installApp(uri: Uri) {
viewModelScope.launch(context = exceptionHandler + DEFAULT) {
val promptInstall = Intent(Intent.ACTION_VIEW, uri)
promptInstall.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
promptInstall.setDataAndType(uri, "application/vnd.android" + ".package-archive")
promptInstall.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
promptInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityMutableLiveData.postValue(promptInstall.toOneTimeEvent())
}
}
and then in my fragment :
viewModel.startActivityLiveData.observe(viewLifecycleOwner, Observer { oneTimeEvent ->
(oneTimeEvent.getValue() as Intent).startActivityFromIntent(requireActivity())})
And finally this is my extension function :
fun Intent.startActivityFromIntent(context: Context) = (context as Activity).startActivity(this)
Well, it seems that the process I mentioned above has a problem(it crashed!) in Android 10 and above. Here is the solution I found:
private fun Uri.installApp(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!context.packageManager.canRequestPackageInstalls()) {
startForResult.launch(Intent(ACTION_MANAGE_UNKNOWN_APP_SOURCES))
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(
} else {
viewModel.installApp(this)
}
} else {
viewModel.installApp(this)
}
and we must do as below :
private val startForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) {
RESULT_OK -> {
// your logic...
}
RESULT_CANCELED -> {
//do something
}
else -> {
}
}
}
Related
I was working on an update for my app, a section of which deals with downloading and installing an APK file.
As long as the previous version were targeting SDK 30 everything worked pretty smoothly. But as soon as I incremented the target and compile SDK to 32 it just started behaving queerly.
Here is the code that deals which the package manager and the installation:
private fun installOnClickListener() {
binding.termuxInstallCard.showProgress()
var session: PackageInstaller.Session? = null
try {
val packageInstaller: PackageInstaller =
requireContext().packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
)
val sessionId = packageInstaller.createSession(params)
session = packageInstaller.openSession(sessionId)
viewModel.addApkToSession(session)
var installBroadcast: PendingIntent? = null
val intent =
Intent(PACKAGE_INSTALLED_ACTION).putExtra(
"packageName",
"com.termux"
)
installBroadcast = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(
context,
0,
intent,
FLAG_MUTABLE
)
} else {
PendingIntent.getBroadcast(context, 0, intent, FLAG_UPDATE_CURRENT)
}
session.commit(installBroadcast.intentSender)
session.close()
} catch (e: IOException) {
throw RuntimeException("Couldn't install package", e)
} catch (e: RuntimeException) {
session?.abandon()
throw e
} finally {
session?.close()
}
}
Here is what is happening:
As I am targeting SDK 32, I am required to specify the Mutability of PendingIntent
Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent
When I use:
FLAG_MUTABLE- The installation just fails stating the error code- STATUS_FAILURE_INVALID with no extra message for debugging in EXTRA_STATUS_MESSAGE. The thing is that when I try to install the same downloaded APK via the adb shell, it just installs normally without any issues.
FLAG_IMMUTABLE- The installation succeeds without prompting user with the installation dialog but nothing is actually installed.
More code in case you need it-
fun addApkToInstallSession(
path: String,
session: PackageInstaller.Session
) {
val file = File("${context.filesDir.path}/$path")
val packageInSession: OutputStream = session.openWrite("com.termux", 0, -1)
val inputStream = FileInputStream(file)
val byteStream = inputStream.read()
try {
var c: Int
val buffer = ByteArray(16384)
while (inputStream.read(buffer).also { c = it } >= 0) {
packageInSession.write(buffer, 0, c)
}
} catch (e: IOException) {
println("IOEX")
} finally {
try {
packageInSession.close()
inputStream.close()
} catch (e: IOException) {
println("IOEX in closing the stream")
}
}
}
private val broadcastReceiverForInstallEvents = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
lifecycleScope.launch(Dispatchers.IO) {
val extras = intent.extras
val status = extras!!.getInt(PackageInstaller.EXTRA_STATUS)
val packageName = extras.getString("packageName")!!
if (PACKAGE_INSTALLED_ACTION == intent.action) {
println("STATUS $status")
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
try {
val confirmIntent = extras[Intent.EXTRA_INTENT] as Intent
confirmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(confirmIntent)
} catch (e: Exception) {
lifecycleScope.launch(Dispatchers.Main) {
Toast.makeText(
requireContext(),
"We could not find an application to handle the installation of apps. Please download a package installer.",
Toast.LENGTH_SHORT
).show()
}
}
}
PackageInstaller.STATUS_SUCCESS -> {
lifecycleScope.launch(Dispatchers.Main) {
println("$packageName Install succeeded!")
// todo all done animation
binding.termuxInstallCard.markAsComplete()
Toast.makeText(requireContext(), "All Done!", Toast.LENGTH_SHORT)
.show()
lifecycleScope.launch {
// viewModel.setTermuxSetupDone()
}
/* redirecting... */
Handler(Looper.getMainLooper()).postDelayed({
redirect()
}, 2000)
}
}
PackageInstaller.STATUS_FAILURE, PackageInstaller.STATUS_FAILURE_ABORTED, PackageInstaller.STATUS_FAILURE_BLOCKED, PackageInstaller.STATUS_FAILURE_CONFLICT, PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, PackageInstaller.STATUS_FAILURE_INVALID, PackageInstaller.STATUS_FAILURE_STORAGE -> {
lifecycleScope.launch(Dispatchers.Main) {
println("Extra Status Message${extras.getString("EXTRA_STATUS_MESSAGE")}")
"There was an error installing Termux. Please retry.".showSnackbar(
binding.root,
true
)
binding.termuxInstallCard.hideProgress()
}
}
else -> {
lifecycleScope.launch(Dispatchers.Main) {
println("$packageName Install failed else!")
// exitActivity("Package failed to install -> Unknown Error!")
binding.termuxInstallCard.hideProgress()
}
}
}
}
}
}
}
I would really appreciate some help!
In the past when I tried to install APKs I used either Knox on Samsung devices or I'd refer the user to install the package via package manager, this is the code I've used:
public static void cleanInstall(String datum, Context context) {
File file = new File(datum);
if (file.exists()) {
Intent intent = new Intent(Intent.ACTION_VIEW);
String type = "application/vnd.android.package-archive";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri downloadedApk = FileProvider.getUriForFile(context, ".MyPackage.myFileProvider", file);
intent.setDataAndType(downloadedApk, type);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
intent.setDataAndType(Uri.fromFile(file), type);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
} else {
Toast.makeText(context, "ّFile not found!", Toast.LENGTH_SHORT).show();
}
}
I've not used this code in a while so I don't know if it works anymore but it's something you might want to check, to see if it does. Make sure the path you are referring to does not reside on SD as you'll probably face permission denied issues.
On calculating the hash of the file I was committing to the session of the Package Manager, I found out that it didn't write the APK file correctly.
val inputStream: InputStream = session.openRead("com.termux")
val file = File("${requireContext().filesDir.path}/test2.apk")
if (!file.exists()) {
file.createNewFile()
}
try {
inputStream.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
} catch (e: Exception) {
println("Exception occured $e")
}
if (file.length() > 0) {
val hash = file.getMD5Hash()
println("HASH of session - $hash")
}
Fixed that and with the combination of the Mutable pending intent. The package is now installing perfectly.
private val sdkForResultLauncher: ActivityResultLauncher<Intent> =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
val jumioResult: JumioResult =
result.data?.getSerializableExtra(JumioActivity.EXTRA_RESULT) as JumioResult
Log.d(TAG, "AccountId: ${jumioResult.accountId}")
Log.d(TAG, "WorkflowExecutionId: ${jumioResult.workflowExecutionId}")
if (jumioResult.isSuccess) {
jumioResult.credentialInfos?.forEach {
when (jumioResult.getResult(it)) {
is JumioIDResult -> { //check your id result here
}
is JumioFaceResult -> { //check your face result here
}
}
}
} else {
jumioResult.error?.let {
Log.d(TAG, it.message)
}
}
}
I'm using Jumio android SDK version 4.0.0.Above is the Jumio Android SDK sample code, I am able to upload ID proof and face selfie but I am getting NULL values in JumioIDResult and JumioFaceResult.
Thanks in advance!!
Situation
I submit data setTripDeliver, the collect works fine (trigger LOADING and then SUCCESS). I pressed a button go to next fragment B (using replace). After that, I press back button (using popbackstack). the collect SUCCESS triggered.
Codes Related
These codes at the FragmentA.kt inside onViewCreated.
private fun startLifeCycle() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
}
}
}
These codes when to submit data at a button setOnClickListener.
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.setTripDeliver(
verificationCode,
remark
)
}
Method to collect flow collectTripReattempt()
private suspend fun collectTripReattempt() {
viewModel.tripReattempt.collect {
when (it) {
is Resource.Initialize -> {
}
is Resource.Loading -> {
Log.i("???","collectTripReattempt loading")
handleSaveEarly()
}
is Resource.Success -> {
val error = it.data?.error
if (error == null) {
Tools.showToast(requireContext(), "Success Reattempt")
Log.i("???","collectTripReattempt Success")
} else {
Tools.showToast(requireContext(), "$error")
}
handleSaveEnding()
}
is Resource.Error -> {
handleSaveEnding()
}
}
}
}
Below codes are from ViewModel.
private val _tripDeliver =
MutableStateFlow<Resource<TripDeliverResponse>>(Resource.Initialize())
val tripDeliver: StateFlow<Resource<TripDeliverResponse>> = _tripDeliver
This method to call repository.
suspend fun setTripDeliver(
verificationCode: String?,
remark: String?
) {
_tripDeliver.value = Resource.Loading()
try {
val result = withContext(ioDispatcher) {
val tripDeliverParameter = DeliverParameter(
verificationCode,
remark
)
val response = appRepository.setTripDeliver(tripDeliverParameter)
Resource.getResponse { response }
}
_tripDeliver.value = result
} catch (e: Exception) {
when (e) {
is IOException -> _tripDeliver.value =
Resource.Error(messageInt = R.string.no_internet_connection)
else -> _tripDeliver.value =
Resource.Error("Trip Deliver Error: " + e.message)
}
}
}
Logcat
2021-07-09 19:56:10.946 7446-7446/com.package.app I/???: collectTripReattempt loading
2021-07-09 19:56:11.172 7446-7446/com.package.app I/???: collectTripReattempt Success
2021-07-09 19:56:17.703 7446-7446/com.package.app I/???: collectTripReattempt Success
As you can see, the last Success is called again AFTER I pressed back button (popbackstack)
Question
How to make it trigger once only? Is it the way I implement it is wrong? Thank you in advance.
This is not problem of your implementation this is happening because of stateIn() which use used in your viewModel to convert regular flow into stateFlow
If according to your code snippet the success is triggered once again, then why not loading has triggered?
as per article, it is showing the latest cached value when you left the screen and came back you got the latest cached value on view.
Resource:
https://medium.com/androiddevelopers/migrating-from-livedata-to-kotlins-flow-379292f419fb
The latest value will still be cached so that when the user comes back to it, the view will have some data immediately.
I have found the solution, thanks to #Nurseyit Tursunkulov for giving me a clue. I have to use SharedFlow.
At the ViewModel, I replace the initialize with these:
private val _tripDeliver = MutableSharedFlow<Resource<TripDeliverResponse>>(replay = 0)
val tripDeliver: SharedFlow<Resource<TripDeliverResponse>> = _tripDeliver
At the replay I have to use 0, so this SharedFlow will trigger once. Next, change _tripDeliver.value to _tripDeliver.emit() like the codes below:
fun setTripDeliver(
verificationCode: String?,
remark: String?
) = viewModelScope.launch {
_tripDeliver.emit(Resource.Loading())
if (verificationCode == null && remark == null) {
_tripDeliver.emit(Resource.Error("Remark cannot be empty if verification is empty"))
return#launch
}
try {
val result = withContext(ioDispatcher) {
val tripDeliverParameter = DeliverParameter(
verificationCode,
remark,
)
val response = appRepository.setTripDeliver(tripDeliverParameter)
Resource.getResponse { response }
}
_tripDeliver.emit(result)
} catch (e: Exception) {
when (e) {
is IOException -> _tripDeliver.emit(Resource.Error(messageInt = R.string.no_internet_connection))
else -> _tripDeliver.emit(Resource.Error("Trip Deliver Error: " + e.message))
}
}
}
I hope this answer will help the others also.
I think this is because of coldFlow, you need a HotFlow. Another option is to try to hide and show fragment, instead of replacing. And yet another solution is to keep this code in viewModel.
In my opinion, I think your way of using coroutines in lifeScope is incorrect. After the lifeScope status of FragmentA is at Started again, the coroutine will be restarted:
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
So I think: You need to modify this way:
private fun startLifeCycle() {
viewLifecycleOwner.lifecycleScope.launch {
launch {
collectTripDeliver()
}
launch {
collectTripReattempt()
}
}
}
Facing an issue with IMMEDIATE app update mode. After successful completion of app update, everything is closed and not restarting the app. That is the issue.
But android documentation says:
A full screen user experience that requires the user to update and
restart the app in order to continue using the app. This UX is best
for cases where an update is critical for continued use of the app.
After a user accepts an immediate update, Google Play handles the
update installation and app restart.
implementation 'com.google.android.play:core:1.9.1'
implementation 'com.google.android.play:core-ktx:1.8.1'
code
class MainActivity : AppCompatActivity() {
companion object {
const val UPDATE_REQUEST_CODE = 112
const val TAG = "MainActivity"
}
private var appUpdateManager: AppUpdateManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.tv_text).text = "Version " + BuildConfig.VERSION_NAME
// Returns an intent object that you use to check for an update.
appUpdateManager = AppUpdateManagerFactory.create(this)
}
private val listener: InstallStateUpdatedListener =
InstallStateUpdatedListener { installState ->
if (installState.installStatus() == InstallStatus.DOWNLOADED) {
// After the update is downloaded, show a notification
// and request user confirmation to restart the app.
Log.d(TAG, "An update has been downloaded")
} else if (installState.installStatus() == InstallStatus.INSTALLED) {
Log.d(TAG, "An update has been installed")
}
}
override fun onStart() {
super.onStart()
checkAppVersionNew()
}
private fun checkAppVersionNew() {
val appUpdateInfoTask = appUpdateManager!!.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { result: AppUpdateInfo ->
if (result.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && result.isUpdateTypeAllowed(
AppUpdateType.IMMEDIATE
)
) {
try {
Log.d(TAG, "An update available")
appUpdateManager!!.startUpdateFlowForResult(
result,
AppUpdateType.IMMEDIATE,
this,
UPDATE_REQUEST_CODE
)
} catch (e: SendIntentException) {
Log.d(TAG, "SendIntentException $e")
e.printStackTrace()
}
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == UPDATE_REQUEST_CODE) {
when (resultCode) {
RESULT_OK -> {
Log.d(TAG, "Update success")
}
RESULT_CANCELED -> {
Log.d(TAG, "Update cancelled")
}
ActivityResult.RESULT_IN_APP_UPDATE_FAILED -> {
Log.d(TAG, "Update failed")
}
}
}
}
}
I faced this issue and after 2 days I added
android:launchMode="singleTask"
to the launcher Activity And I used
ProcessPhoenix.triggerRebirth(this)
From ProcessPhoenix library And then the app restarted after updating.
I trying to new registerForActivityResult for taking picture. I can open Camera Intent, but after taking picture, callback is not triggered and i can't see anything about Activity Result or an error on logcat.
I tried also RequestPermission, it's triggered. I couldn't find, what's wrong.
My code is here:
class UploadDocumentFragment {
private val registerTakePicture = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { isSuccess ->
if (isSuccess) {
viewModel.addDocToRequest()
viewModel.setSelectedDocument(null)
} else {
R.string.internal_error.showAsDialog { }
}
}
//...
private fun takeImage() {
val photoFile: File? = viewModel.createImageFile()
photoFile?.also {
val photoURI: Uri = FileProvider.getUriForFile(
requireContext(),
BuildConfig.APPLICATION_ID +".fileProvider",
it
)
registerTakePicture.launch(photoURI)
}
}
}
createImageFile function on ViewModel:
#Throws(IOException::class)
fun createImageFile(): File? {
val imageFileName = selectedDocumentTypeLD.value?.visibleName
return try {
val file = File(storageDir, "$imageFileName.jpg")
if (file.createNewFile() || file.exists()) {
file
} else {
null
}
} catch (ex: IOException) {
ex.printStackTrace()
null
}
}
App gradle:
implementation 'androidx.activity:activity-ktx:1.2.0-alpha06'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06'
I found the problem.
My previous onActivityResult function was still there. I thought maybe the old function could override registerForActivityResult. When I remove the old function, registerForActivityResult works very well.
In my case, problem was android:noHistory="true" in manifest in the container activity (activity that contains all fragments).
Removing this called registerForActivityResult() in fragment.