feat: add action to launch installed application (fixes #1336)

This commit is contained in:
Akash Yadav
2023-10-06 00:15:41 +05:30
parent 57c89a3f18
commit 3816ec5866
8 changed files with 287 additions and 51 deletions
@@ -26,10 +26,9 @@ import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.actions.getContext
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireContext
import com.itsaky.androidide.actions.openApplicationModuleChooser
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.models.ApkMetadata
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.resources.R
@@ -37,7 +36,6 @@ import com.itsaky.androidide.tasks.executeAsyncProvideError
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import com.itsaky.androidide.tooling.api.models.BasicAndroidVariantMetadata
import com.itsaky.androidide.utils.ApkInstaller
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.InstallationResultHandler
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.resolveAttr
@@ -51,7 +49,8 @@ import java.io.File
*
* @author Akash Yadav
*/
class QuickRunWithCancellationAction(context: Context, override val order: Int) : BaseBuildAction() {
class QuickRunWithCancellationAction(context: Context, override val order: Int) :
BaseBuildAction() {
init {
label = context.getString(R.string.quick_run_debug)
@@ -102,13 +101,13 @@ class QuickRunWithCancellationAction(context: Context, override val order: Int)
}
private fun quickRun(data: ActionData): Boolean {
chooseApplication(data) { module ->
openApplicationModuleChooser(data) { module ->
val activity = data.requireActivity()
val variant = module.getSelectedVariant() ?: run {
activity.flashError(
activity.getString(R.string.err_selected_variant_not_found))
return@chooseApplication
return@openApplicationModuleChooser
}
val taskName = "${module.path}:${variant.mainArtifact.assembleTaskName}"
@@ -195,36 +194,6 @@ class QuickRunWithCancellationAction(context: Context, override val order: Int)
install(data, apkFile)
}
private fun chooseApplication(data: ActionData, callback: (AndroidModule) -> Unit) {
val projectManager = IProjectManager.getInstance()
val applications = projectManager.getAndroidAppModules()
if (applications.isEmpty()) {
log.error("Cannot run application. No application modules found in project.")
return
}
if (applications.size == 1) {
// Only one application module in available in the project.
callback(applications.first())
return
}
// there are multiple application modules in the project
// ask the user to select the application module to build
val context = data.requireContext()
val builder = DialogUtils.newSingleChoiceDialog(
context,
context.getString(com.itsaky.androidide.R.string.title_choose_application),
applications.map { it.path }.toTypedArray(),
0) { selection ->
val app = applications[selection]
log.info("Selected application: '${app.path}'")
callback(app)
}
builder.show()
}
private fun install(data: ActionData, apk: File) {
val activity =
data.getActivity()
@@ -0,0 +1,78 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.actions.etc
import android.content.Context
import android.view.MenuItem
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.openApplicationModuleChooser
import com.itsaky.androidide.builder.model.UNKNOWN_PACKAGE
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.utils.IntentUtils
import com.itsaky.androidide.utils.flashError
/**
* An action to launch the already installed application on the device.
*
* @author Akash Yadav
*/
class LaunchAppAction(context: Context, override val order: Int) : EditorActivityAction() {
override val id: String = "ide.editor.launchInstalledApp"
override var requiresUIThread: Boolean = true
init {
label = context.getString(R.string.title_launch_app)
icon = ContextCompat.getDrawable(context, R.drawable.ic_open_external)
}
override fun prepare(data: ActionData) {
data.getActivity() ?: run {
markInvisible()
return
}
visible = true
val projectManager = IProjectManager.getInstance()
enabled = projectManager.getAndroidAppModules().isNotEmpty()
}
override fun execAction(data: ActionData) {
openApplicationModuleChooser(data) { app ->
val packageName = app.packageName
if (packageName == UNKNOWN_PACKAGE) {
flashError(R.string.err_cannot_determine_package)
return@openApplicationModuleChooser
}
val activity = data.requireActivity()
IntentUtils.launchApp(activity, packageName)
}
}
override fun getShowAsActionFlags(data: ActionData): Int {
// prefer showing this in the overflow menu
return MenuItem.SHOW_AS_ACTION_IF_ROOM
}
}
@@ -0,0 +1,70 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.actions
import android.content.Context
import com.itsaky.androidide.R
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.flashError
/**
* @see openApplicationModuleChooser
*/
fun openApplicationModuleChooser(data: ActionData, callback: (AndroidModule) -> Unit) =
openApplicationModuleChooser(data.requireContext(), callback)
/**
* Shows a dialog to let the user choose between Android application modules in case the project has
* multiple subproject with `com.android.application` plugin. If the project contains only a single
* application module, it is selected by default and the dialog is not shown to the user.
*
* @param
*/
fun openApplicationModuleChooser(context: Context, callback: (AndroidModule) -> Unit) {
val log = ILogger.newInstance("ActionUtils")
val projectManager = IProjectManager.getInstance()
val applications = projectManager.getAndroidAppModules()
if (applications.isEmpty()) {
flashError(R.string.msg_launch_failure_no_app_module)
log.error("Cannot run application. No application modules found in project.")
return
}
if (applications.size == 1) {
// Only one application module in available in the project.
callback(applications.first())
return
}
// there are multiple application modules in the project
// ask the user to select the application module to build
val builder = DialogUtils.newSingleChoiceDialog(
context,
context.getString(R.string.title_choose_application),
applications.map { it.path }.toTypedArray(),
0) { selection ->
val app = applications[selection]
log.info("Selected application: '${app.path}'")
callback(app)
}
builder.show()
}
@@ -77,6 +77,7 @@ import com.itsaky.androidide.utils.ApkInstallationSessionCallback
import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.InstallationResultHandler.onResult
import com.itsaky.androidide.utils.IntentUtils
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.viewmodel.EditorViewModel
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
@@ -190,12 +191,12 @@ abstract class BaseEditorActivity :
}
if (launchAppAfterInstall) {
tryLaunchApp(packageName)
IntentUtils.launchApp(this, packageName)
return
}
Snackbar.make(binding.realContainer, string.msg_action_open_application, Snackbar.LENGTH_LONG)
.setAction(string.yes) { tryLaunchApp(packageName) }
.setAction(string.yes) { IntentUtils.launchApp(this, packageName) }
.show()
}
@@ -370,18 +371,6 @@ abstract class BaseEditorActivity :
editorViewModel.statusGravity = gravity
}
private fun tryLaunchApp(packageName: String) {
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
launchIntent?.let {
try {
startActivity(it)
} catch (e: Throwable) {
flashError(string.msg_app_launch_failed)
log.error("Failed to launch application with package name '$packageName'", e)
}
}
}
private fun checkIsDestroying() {
if (!isDestroying && isFinishing) {
isDestroying = true
@@ -33,6 +33,7 @@ import com.itsaky.androidide.actions.editor.SelectAllAction
import com.itsaky.androidide.actions.etc.DisconnectLogSendersAction
import com.itsaky.androidide.actions.etc.FileTreeAction
import com.itsaky.androidide.actions.etc.FindActionMenu
import com.itsaky.androidide.actions.etc.LaunchAppAction
import com.itsaky.androidide.actions.etc.PreviewLayoutAction
import com.itsaky.androidide.actions.etc.ReloadColorSchemesAction
import com.itsaky.androidide.actions.file.CloseAllFilesAction
@@ -76,6 +77,7 @@ class EditorActivityActions {
registry.registerAction(ProjectSyncAction(context, order++))
registry.registerAction(ReloadColorSchemesAction(context, order++))
registry.registerAction(DisconnectLogSendersAction(context, order++))
registry.registerAction(LaunchAppAction(context, order++))
// editor text actions
registry.registerAction(ExpandSelectionAction(context, order++))
@@ -19,10 +19,13 @@ package com.itsaky.androidide.utils
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import com.blankj.utilcode.util.ImageUtils
import com.blankj.utilcode.util.ImageUtils.ImageType.TYPE_UNKNOWN
import com.itsaky.androidide.R
import java.io.File
/**
@@ -32,11 +35,13 @@ import java.io.File
*/
object IntentUtils {
private const val RESULT_LAUNCH_APP_INTENT_SENDER = 223
@JvmStatic
fun openImage(context: Context, file: File) {
imageIntent(context = context, file = file, intentAction = Intent.ACTION_VIEW)
}
@JvmStatic
@JvmOverloads
fun imageIntent(
@@ -83,4 +88,50 @@ object IntentUtils {
context.startActivity(Intent.createChooser(intent, null))
}
/**
* Launch the application with the given [package name][packageName].
*
* @param context The context that will be used to fetch the launch intent.
* @param packageName The package name of the application.
*/
fun launchApp(context: Context, packageName: String) {
if (Build.VERSION.SDK_INT >= 33) {
launchAppApi33(context, packageName)
return
}
doLaunchApp(context, packageName)
}
private fun doLaunchApp(context: Context, packageName: String) {
try {
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
requireNotNull(launchIntent) {
"Cannot get launch intent for package '$packageName'. Is the application installed?"
}
context.startActivity(launchIntent)
} catch (e: Throwable) {
flashError(R.string.msg_app_launch_failed)
ILogger.instance().error("Failed to launch application with package name '$packageName'", e)
}
}
@RequiresApi(33)
private fun launchAppApi33(context: Context, packageName: String) {
try {
val sender = context.packageManager.getLaunchIntentSenderForPackage(packageName)
sender.sendIntent(
context,
RESULT_LAUNCH_APP_INTENT_SENDER,
null,
null,
null
)
} catch (e: Throwable) {
flashError(R.string.msg_app_launch_failed)
ILogger.instance().error("Failed to launch application with package name '$packageName'", e)
}
}
}
@@ -608,4 +608,6 @@
<string name="idepref_launchAppAfterInstall_summary">If enabled, the IDE will (without confirmation) launch the application right after it is installed.</string>
<string name="title_run_options">Run options</string>
<string name="err_cannot_determine_package">Cannot run app. Unable to determine package name.</string>
<string name="msg_launch_failure_no_app_module">Cannot run application. No application modules found in project.</string>
<string name="title_launch_app">Launch app</string>
</resources>
+75
View File
@@ -0,0 +1,75 @@
// DO NOT EDIT - Automatically generated file
plugins {
id 'com.android.application'
}
android {
namespace = "com.itsaky.test.app"
compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
defaultConfig {
applicationId rootProject.ext.appId
minSdkVersion rootProject.ext.minSdk
targetSdkVersion rootProject.ext.targetSdk
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
lintOptions {
abortOnError false
}
buildFeatures {
viewBinding true
}
}
task thisIsATestTask () {
println "This is a test task"
}
dependencies {
implementation fileTree (dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.5.1'
// Issue #488
implementation 'com.unity3d.ads:unity-ads:4.4.0'
implementation 'com.google.android.gms:play-services-ads:21.2.0'
implementation platform('com.google.firebase:firebase-bom:30.5.0')
implementation 'com.google.firebase:firebase-perf'
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-crashlytics'
implementation 'com.google.firebase:firebase-auth'
implementation 'com.google.firebase:firebase-firestore'
implementation 'com.google.android.gms:play-services-auth:20.3.0'
implementation 'com.google.firebase:firebase-messaging'
implementation 'com.google.firebase:firebase-database'
implementation fileTree(dir: 'libs', include: ['*.aar'])
implementation 'com.google.android.gms:play-services-appset:16.0.2'
implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
implementation 'com.google.android.gms:play-services-basement:18.1.0'
implementation 'com.google.firebase:firebase-inappmessaging-display'
implementation 'com.google.android.material:material:1.8.0-alpha01'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation project (path: ':another-android-library')
implementation platform('com.google.firebase:firebase-bom:30.5.0')
implementation 'unresolved:unresolved:unresolved'
}