feat: replace one-time dialogs and permissions with app intro (fixes #1399) (#1541)

Co-authored-by: Akash Yadav <itsaky01@gmail.com>
This commit is contained in:
Vivek Kumar Sahani
2023-12-21 15:38:00 +05:30
committed by GitHub
parent 70ba8a54c7
commit d942d81b5c
178 changed files with 8023 additions and 502 deletions
+1
View File
@@ -118,6 +118,7 @@ dependencies {
implementation(projects.gradlePluginConfig)
implementation(projects.idestats)
implementation(projects.subprojects.aaptcompiler)
implementation(projects.subprojects.appintro)
implementation(projects.subprojects.javacServices)
implementation(projects.subprojects.javapoet)
implementation(projects.subprojects.xmlUtils)
+25 -21
View File
@@ -4,6 +4,27 @@
android:installLocation="internalOnly"
tools:ignore="Instantiatable">
<permission
android:description="@string/permdesc_bind_logger_service"
android:label="@string/permlab_bind_logger_service"
android:name="com.itsaky.androidide.permission.BIND_LOG_SERVICE"
android:protectionLevel="normal"
tools:ignore="ManifestOrder" />
<uses-permission android:name="com.itsaky.androidide.permission.BIND_LOG_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="false"
android:allowNativeHeapPointerTagging="false"
@@ -18,7 +39,7 @@
android:resizeableActivity="false"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidIDE"
tools:ignore="UnusedAttribute,MediaCapabilities">
tools:ignore="DiscouragedApi,MediaCapabilities,UnusedAttribute">
<activity
android:exported="true"
android:name=".activities.CrashHandlerActivity"
@@ -28,15 +49,16 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:exported="true"
android:name=".activities.SplashActivity"
android:theme="@style/AppTheme.Splash">
android:name=".activities.OnboardingActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:exported="false"
android:name=".activities.MainActivity"
@@ -112,22 +134,4 @@
android:value="true" />
</service>
</application>
<permission
android:description="@string/permdesc_bind_logger_service"
android:label="@string/permlab_bind_logger_service"
android:name="com.itsaky.androidide.permission.BIND_LOG_SERVICE"
android:protectionLevel="normal" />
<uses-permission android:name="com.itsaky.androidide.permission.BIND_LOG_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
@@ -53,11 +53,11 @@ class TerminalSidebarAction(context: Context, override val order: Int) : Abstrac
val context = data.requireContext()
val intent = Intent(context, TerminalActivity::class.java).apply {
putExtra(
TerminalActivity.KEY_WORKING_DIRECTORY,
TERMUX_ACTIVITY.EXTRA_SESSION_WORKING_DIR,
Objects.requireNonNull(IProjectManager.getInstance().projectDirPath)
)
putExtra(
TerminalActivity.KEY_SESSION_NAME,
TERMUX_ACTIVITY.EXTRA_SESSION_NAME,
IProjectManager.getInstance().rootProject?.rootProject?.name
)
putExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, isFailsafe)
@@ -21,16 +21,12 @@ import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import androidx.transition.doOnEnd
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.transition.MaterialSharedAxis
import com.itsaky.androidide.activities.editor.EditorActivityKt
import com.itsaky.androidide.app.LimitlessIDEActivity
@@ -40,15 +36,12 @@ import com.itsaky.androidide.preferences.internal.NO_OPENED_PROJECT
import com.itsaky.androidide.preferences.internal.autoOpenProjects
import com.itsaky.androidide.preferences.internal.confirmProjectOpen
import com.itsaky.androidide.preferences.internal.lastOpenedProject
import com.itsaky.androidide.preferences.internal.prefManager
import com.itsaky.androidide.preferences.internal.statConsentDialogShown
import com.itsaky.androidide.preferences.internal.statOptIn
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.templates.ITemplateProvider
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashInfo
import com.itsaky.androidide.viewmodel.MainViewModel
import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_MAIN
@@ -88,21 +81,7 @@ class MainActivity : LimitlessIDEActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!checkDeviceSupported()) {
return
}
showStatConsentDialogIfNeeded {
app.reportStatsIfNecessary()
if (!checkToolsIsInstalled()) {
showDialogInstallJdkSdk()
} else {
openLastProject()
}
}
openLastProject()
viewModel.currentScreen.observe(this) { screen ->
if (screen == -1) {
@@ -125,30 +104,8 @@ class MainActivity : LimitlessIDEActivity() {
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
private fun showStatConsentDialogIfNeeded(onResult: () -> Unit) {
if (statConsentDialogShown) {
onResult()
return
}
DialogUtils.newMaterialDialogBuilder(this).apply {
setTitle(string.title_androidide_statistics)
setMessage(string.msg_androidide_statistics)
setCancelable(false)
setPositiveButton(string.btn_opt_in) { dialog, _ ->
dialog.dismiss()
statOptIn = true
}
setNegativeButton(string.btn_no_thanks) { dialog, _ ->
dialog.dismiss()
statOptIn = false
}
setOnDismissListener {
statConsentDialogShown = true
onResult()
}
show()
}
private fun openOnboarding() {
startActivity(Intent(this, OnboardingActivity::class.java))
}
override fun onInsetsUpdated(insets: Rect) {
@@ -194,58 +151,11 @@ class MainActivity : LimitlessIDEActivity() {
}
}
override fun onStorageDenied() {
flashError(string.msg_storage_denied)
finishAffinity()
}
override fun bindLayout(): View {
_binding = ActivityMainBinding.inflate(layoutInflater)
return binding.root
}
private fun showDialogInstallJdkSdk() {
val dp24 = SizeUtils.dp2px(24f)
val builder = DialogUtils.newMaterialDialogBuilder(this)
builder.setTitle(string.title_warning)
val view = TextView(this)
view.setPaddingRelative(dp24, dp24, dp24, dp24)
view.text = HtmlCompat.fromHtml(getString(string.msg_require_install_jdk_and_android_sdk),
HtmlCompat.FROM_HTML_MODE_COMPACT)
view.movementMethod = LinkMovementMethod.getInstance()
builder.setView(view)
builder.setCancelable(false)
builder.setPositiveButton(android.R.string.ok) { _, _ -> openTerminal() }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> finishAffinity() }
builder.setNeutralButton(string.btn_docs) { _, _ -> app.openDocs(); finishAffinity() }
builder.show()
}
private fun openTerminal() {
startActivity(Intent(this, TerminalActivity::class.java))
}
private fun checkDeviceSupported(): Boolean {
val configProvider = IDEBuildConfigProvider.getInstance()
if (!configProvider.supportsBuildFlavor()) {
DialogUtils.showUnSupportedDevice(this, configProvider.flavorArch.abi,
configProvider.deviceArch.abi)
return false
}
if (configProvider.flavorArch != configProvider.deviceArch) {
// IDE's build flavor is NOT the primary arch of the device
// warn the user
if (!prefManager.getBoolean("ide.archConfig.experimentalWarning.isShown", false)) {
DialogUtils.showExperimentalArchSupportWarning(this, configProvider.flavorArch.abi,
configProvider.deviceArch.abi)
}
}
return true
}
private fun openLastProject() {
binding.root.post { tryOpenLastProject() }
}
@@ -295,10 +205,6 @@ class MainActivity : LimitlessIDEActivity() {
startActivity(Intent(this, EditorActivityKt::class.java))
}
private fun checkToolsIsInstalled(): Boolean {
return Environment.JAVA.exists() && Environment.ANDROID_HOME.exists()
}
override fun onDestroy() {
ITemplateProvider.getInstance().release()
super.onDestroy()
@@ -0,0 +1,177 @@
/*
* 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.activities
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.github.appintro.AppIntro2
import com.github.appintro.AppIntroPageTransformerType
import com.itsaky.androidide.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider
import com.itsaky.androidide.fragments.onboarding.GreetingFragment
import com.itsaky.androidide.fragments.onboarding.IdeSetupConfigurationFragment
import com.itsaky.androidide.fragments.onboarding.OnboardingInfoFragment
import com.itsaky.androidide.fragments.onboarding.PermissionsFragment
import com.itsaky.androidide.fragments.onboarding.StatisticsFragment
import com.itsaky.androidide.preferences.internal.prefManager
import com.itsaky.androidide.preferences.internal.statConsentDialogShown
import com.itsaky.androidide.ui.themes.IThemeManager
import com.itsaky.androidide.utils.Environment
class OnboardingActivity : AppIntro2() {
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
finishAffinity()
}
}
companion object {
private const val KEY_ARCHCONFIG_WARNING_IS_SHOWN = "ide.archConfig.experimentalWarning.isShown"
}
override fun onCreate(savedInstanceState: Bundle?) {
IThemeManager.getInstance().applyTheme(this)
super.onCreate(savedInstanceState)
if (isSetupDone()) {
openActivity(MainActivity::class.java)
finish()
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
setSwipeLock(true)
setTransformer(AppIntroPageTransformerType.Fade)
setProgressIndicator()
showStatusBar(true)
isIndicatorEnabled = true
isWizardMode = true
addSlide(GreetingFragment())
if (!checkDeviceSupported()) {
return
}
if (!statConsentDialogShown) {
addSlide(StatisticsFragment.newInstance(this))
statConsentDialogShown = true
}
if (!PermissionsFragment.areAllPermissionsGranted(this)) {
addSlide(PermissionsFragment.newInstance(this))
}
if (!checkToolsIsInstalled()) {
addSlide(IdeSetupConfigurationFragment.newInstance(this))
}
}
override fun onResume() {
super.onResume()
if (isSetupDone()) {
openActivity(MainActivity::class.java)
finish()
}
}
override fun onNextPressed(currentFragment: Fragment?) {
(currentFragment as? StatisticsFragment?)?.updateStatOptInStatus()
}
override fun onDonePressed(currentFragment: Fragment?) {
(currentFragment as? StatisticsFragment?)?.updateStatOptInStatus()
if (!IDEBuildConfigProvider.getInstance().supportsBuildFlavor()) {
finishAffinity()
return
}
if (!checkToolsIsInstalled() && currentFragment is IdeSetupConfigurationFragment) {
val intenet = Intent(this, TerminalActivity::class.java)
if (currentFragment.isAutoInstall()) {
intenet.putExtra(TerminalActivity.EXTRA_ONBOARDING_RUN_IDESETUP, true)
intenet.putExtra(TerminalActivity.EXTRA_ONBOARDING_RUN_IDESETUP_ARGS,
currentFragment.buildIdeSetupArguments())
}
startActivity(intenet)
return
}
openActivity(MainActivity::class.java)
}
private fun openActivity(cls: Class<*>) {
startActivity(Intent(this, cls))
}
private fun checkToolsIsInstalled(): Boolean {
return Environment.JAVA.exists() && Environment.ANDROID_HOME.exists()
}
private fun isSetupDone() =
(checkToolsIsInstalled() && statConsentDialogShown && PermissionsFragment.areAllPermissionsGranted(
this))
private fun checkDeviceSupported(): Boolean {
val configProvider = IDEBuildConfigProvider.getInstance()
if (!configProvider.supportsBuildFlavor()) {
addSlide(OnboardingInfoFragment.newInstance(
getString(string.title_unsupported_device),
getString(
string.msg_unsupported_device,
configProvider.flavorArch.abi,
configProvider.deviceArch.abi
),
R.drawable.ic_alert,
ContextCompat.getColor(this, R.color.color_error)
))
return false
}
if (configProvider.flavorArch != configProvider.deviceArch) {
// IDE's build flavor is NOT the primary arch of the device
// warn the user
if (!archConfigExperimentalWarningIsShown()) {
addSlide(OnboardingInfoFragment.newInstance(
getString(string.title_experiment_flavor),
getString(string.msg_experimental_flavor,
configProvider.flavorArch.abi,
configProvider.deviceArch.abi
),
R.drawable.ic_alert,
ContextCompat.getColor(this, R.color.color_warning)
))
prefManager.putBoolean(KEY_ARCHCONFIG_WARNING_IS_SHOWN, true)
}
}
return true
}
private fun archConfigExperimentalWarningIsShown() =
prefManager.getBoolean(KEY_ARCHCONFIG_WARNING_IS_SHOWN, false)
}
@@ -1,66 +0,0 @@
/*
* 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.activities
import android.content.ComponentName
import android.os.Bundle
import android.os.IBinder
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.ILogger
import com.termux.app.TermuxActivity
import com.termux.shared.termux.TermuxConstants
import java.io.File
class TerminalActivity : TermuxActivity() {
companion object {
const val KEY_WORKING_DIRECTORY = TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY.EXTRA_SESSION_WORKING_DIR
const val KEY_SESSION_NAME = TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY.EXTRA_SESSION_NAME
private val LOG = ILogger.newInstance("TerminalActivity")
private val SOURCES_LIST_CONTENT = "deb https://packages.androidide.com/apt/termux-main/ stable main".toByteArray()
}
override val navigationBarColor: Int
get() = ContextCompat.getColor(this, android.R.color.black)
override val statusBarColor: Int
get() = ContextCompat.getColor(this, android.R.color.black)
override fun onCreate(savedInstanceState: Bundle?) {
val controller = WindowCompat.getInsetsController(
window, window.decorView)
controller.isAppearanceLightNavigationBars = false
controller.isAppearanceLightStatusBars = false
super.onCreate(savedInstanceState)
}
override fun onServiceConnected(componentName: ComponentName?, service: IBinder?) {
super.onServiceConnected(componentName, service)
writeSourcesList()
}
private fun writeSourcesList() {
try {
File(Environment.PREFIX, "etc/apt/sources.list").writeBytes(SOURCES_LIST_CONTENT)
} catch (th: Throwable) {
LOG.error("Unable to update sources.list", th)
}
}
}
@@ -0,0 +1,106 @@
/*
* 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.adapters.onboarding
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.LayoutOnboardingItemBinding
import com.itsaky.androidide.models.OnboardingItem
import com.itsaky.androidide.utils.resolveAttr
/**
* Default implmentation of [RecyclerView.Adapter] for showing [OnboardingItem]s.
*
* @author Akash Yadav
*/
open class DefaultOnboardingItemAdapter<T : OnboardingItem>(
protected val items: List<T>,
protected val onItemClickListener: OnItemClickListener<T>? = null,
protected val onItemLongClickListener: OnItemLongClickListener<T>? = null
) : RecyclerView.Adapter<DefaultOnboardingItemAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutOnboardingItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
doBindViewHolder(holder, position, getItem(position), holder.binding)
}
protected open fun doBindViewHolder(
holder: ViewHolder,
position: Int,
item: T,
binding: LayoutOnboardingItemBinding
) {
binding.content.title.text = item.title
if (item.description.isNotBlank()) {
binding.content.subtitle.text = item.description
} else {
binding.content.subtitle.visibility = View.INVISIBLE
}
if (item.icon != 0) {
binding.content.icon.setImageResource(item.icon)
if (item.iconTint != 0) {
binding.content.icon.supportImageTintList = ColorStateList.valueOf(item.iconTint)
}
} else {
binding.content.icon.visibility = View.INVISIBLE
}
binding.root.isClickable = item.isClickable
binding.root.isFocusable = item.isClickable
if (item.isClickable && onItemClickListener != null) {
binding.root.setOnClickListener { onItemClickListener.onClick(item, position, binding) }
}
if (item.isLongClickable && onItemLongClickListener != null) {
binding.root.setOnLongClickListener { onItemLongClickListener.onLongClick(item, position, binding) }
}
}
override fun getItemCount(): Int {
return items.size
}
fun getItem(index: Int): T {
return items[index]
}
class ViewHolder(val binding: LayoutOnboardingItemBinding) :
RecyclerView.ViewHolder(binding.root)
fun interface OnItemClickListener<T : OnboardingItem> {
fun onClick(item: T, position: Int, binding: LayoutOnboardingItemBinding)
}
fun interface OnItemLongClickListener<T : OnboardingItem> {
fun onLongClick(item: T, position: Int, binding: LayoutOnboardingItemBinding): Boolean
}
}
@@ -0,0 +1,80 @@
/*
* 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.adapters.onboarding
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.button.MaterialButton
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.LayoutOnboardingPermissionItemBinding
import com.itsaky.androidide.models.OnboardingPermissionItem
import com.itsaky.androidide.utils.ILogger
/**
* @author Akash Yadav
*/
class OnboardingPermissionsAdapter(private val permissions: List<OnboardingPermissionItem>,
private val requestPermission: (String) -> Unit) :
RecyclerView.Adapter<OnboardingPermissionsAdapter.ViewHolder>() {
class ViewHolder(val binding: LayoutOnboardingPermissionItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutOnboardingPermissionItemBinding.inflate(LayoutInflater.from(parent.context), parent,
false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
val permission = permissions[position]
binding.infoContent.apply {
title.setText(permission.title)
subtitle.setText(permission.description)
}
binding.grantButton.setOnClickListener {
requestPermission(permission.permission)
}
if (permission.isGranted) {
binding.grantButton.apply {
isEnabled = false
text = ""
icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_ok)
iconTint = ColorStateList.valueOf(
ContextCompat.getColor(binding.root.context, R.color.green_500))
iconGravity = MaterialButton.ICON_GRAVITY_TEXT_TOP
iconPadding = 0
iconSize = SizeUtils.dp2px(28f)
}
}
}
private val log = ILogger.newInstance("OnboardingPermissionsAdapter")
override fun getItemCount(): Int {
return permissions.size
}
}
@@ -182,7 +182,7 @@ class IDEApplication : TermuxApplication() {
ExistingPeriodicWorkPolicy.UPDATE, request)
operation.state.observeForever(object : Observer<Operation.State> {
override fun onChanged(t: Operation.State?) {
override fun onChanged(t: Operation.State) {
operation.state.removeObserver(this)
log.debug("reportStatsIfNecessary: WorkManager enqueue result: $t")
}
@@ -239,7 +239,7 @@ class IDEApplication : TermuxApplication() {
val operation = WorkManager.getInstance(this)
.cancelUniqueWork(StatUploadWorker.WORKER_WORK_NAME)
operation.state.observeForever(object : Observer<Operation.State> {
override fun onChanged(t: Operation.State?) {
override fun onChanged(t: Operation.State) {
operation.state.removeObserver(this)
log.info("StatUploadWorker: Cancellation result state: $t")
}
@@ -29,18 +29,18 @@ import com.itsaky.androidide.adapters.TemplateListAdapter
import com.itsaky.androidide.databinding.FragmentTemplateListBinding
import com.itsaky.androidide.templates.ITemplateProvider
import com.itsaky.androidide.templates.ProjectTemplate
import com.itsaky.androidide.utils.FlexboxUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.viewmodel.MainViewModel
import kotlin.math.ceil
/**
* A fragment to show the list of available templates.
*
* @author Akash Yadav
*/
class TemplateListFragment : FragmentWithBinding<FragmentTemplateListBinding>(
R.layout.fragment_template_list, FragmentTemplateListBinding::bind
) {
class TemplateListFragment :
FragmentWithBinding<FragmentTemplateListBinding>(R.layout.fragment_template_list,
FragmentTemplateListBinding::bind) {
private var adapter: TemplateListAdapter? = null
private var layoutManager: FlexboxLayoutManager? = null
@@ -59,30 +59,11 @@ class TemplateListFragment : FragmentWithBinding<FragmentTemplateListBinding>(
// This makes sure that the items are evenly distributed in the list
// and the last row is always aligned to the start
globalLayoutListener = object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
val adapter = this@TemplateListFragment.adapter ?: return
val layoutManager = this@TemplateListFragment.layoutManager ?: return
val columns = layoutManager.flexLinesInternal.firstOrNull()?.itemCount ?: 0
if (columns == 0) {
return
}
val itemCount = adapter.itemCount
val rows = ceil(itemCount.toFloat() / columns.toFloat()).toInt()
if (itemCount % columns == 0) {
return
}
val diff = rows * columns - itemCount
if (diff <= 0) {
return
}
adapter.fillDiff(diff)
}
globalLayoutListener = FlexboxUtils.createGlobalLayoutListenerToDistributeFlexboxItemsEvenly(
{ adapter }, { layoutManager }) { adapter, diff ->
adapter.fillDiff(diff)
}
binding.list.viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
binding.exitButton.setOnClickListener {
@@ -113,8 +94,7 @@ class TemplateListFragment : FragmentWithBinding<FragmentTemplateListBinding>(
// Show only project templates
// reloading the tempaltes also makes sure that the resources are
// released from template parameter widgets
val templates = ITemplateProvider.getInstance(reload = true)
.getTemplates()
val templates = ITemplateProvider.getInstance(reload = true).getTemplates()
.filterIsInstance<ProjectTemplate>()
adapter = TemplateListAdapter(templates) { template, _ ->
@@ -0,0 +1,27 @@
/*
* 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.fragments.onboarding
import com.itsaky.androidide.databinding.FragmentOnboardingGreetingBinding
import com.itsaky.androidide.fragments.FragmentWithBinding
/**
* @author Akash Yadav
*/
class GreetingFragment :
FragmentWithBinding<FragmentOnboardingGreetingBinding>(FragmentOnboardingGreetingBinding::inflate)
@@ -0,0 +1,296 @@
/*
* 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.fragments.onboarding
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Bundle
import android.provider.Settings
import android.text.Html
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.URLSpan
import android.util.Log
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.content.getSystemService
import androidx.core.view.isVisible
import com.github.appintro.SlidePolicy
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.LayoutOnboardngSetupConfigBinding
import com.itsaky.androidide.models.IdeSetupArgument
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.tasks.runOnUiThread
import com.itsaky.androidide.utils.ConnectionInfo
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.getConnectionInfo
/**
* @author Akash Yadav
*/
class IdeSetupConfigurationFragment : OnboardingFragment(), SlidePolicy {
private var _content: LayoutOnboardngSetupConfigBinding? = null
private val content: LayoutOnboardngSetupConfigBinding
get() = checkNotNull(_content) { "Fragment has been destroyed" }
private var backgroundDataRestrictionReceiver: BroadcastReceiver? = null
private var networkStateChangeCallback: NetworkCallback? = null
companion object {
@JvmStatic
fun newInstance(context: Context): IdeSetupConfigurationFragment {
return IdeSetupConfigurationFragment().also {
it.arguments = Bundle().apply {
putCharSequence(KEY_ONBOARDING_TITLE, context.getString(R.string.title_install_tools))
putCharSequence(KEY_ONBOARDING_SUBTITLE,
context.getString(R.string.subtitle_install_tools))
putCharSequence(KEY_ONBOARDING_EXTRA_INFO,
Html.fromHtml(context.getString(R.string.msg_install_tools),
Html.FROM_HTML_MODE_COMPACT))
}
}
}
}
@SuppressLint("PrivateResource")
override fun createContentView(parent: ViewGroup, attachToParent: Boolean) {
_content = LayoutOnboardngSetupConfigBinding.inflate(layoutInflater, parent, attachToParent)
content.apply {
noConnection.root.setText(R.string.msg_no_internet)
cellularConnection.root.setText(R.string.msg_connected_to_cellular)
meteredConnection.root.setText(R.string.msg_connected_to_metered_connection)
backgroundDataRestricted.root.setText(R.string.msg_disable_background_data_restriction)
autoInstallSwitch.setOnCheckedChangeListener { button, isChecked ->
button.setText(
if (isChecked) R.string.action_auto_install else R.string.action_manual_install)
sdkVersionLayout.isEnabled = isChecked
jdkVersionLayout.isEnabled = isChecked
installGit.isEnabled = isChecked
installOpenssh.isEnabled = isChecked
}
val sdkVersions = SdkVersion.entries.map { "SDK ${it.version}" }.reversed()
sdkVersion.setText(sdkVersions[0])
sdkVersion.setAdapter(ArrayAdapter(
requireContext(),
com.google.android.material.R.layout.m3_auto_complete_simple_item,
sdkVersions)
)
val jdkVersions = JdkVersion.entries.map { "JDK ${it.version}" }
jdkVersion.setText(jdkVersions[0])
jdkVersion.setAdapter(ArrayAdapter(
requireContext(),
com.google.android.material.R.layout.m3_auto_complete_simple_item,
jdkVersions)
)
}
updateConnectionStatus()
}
fun isAutoInstall(): Boolean = content.autoInstallSwitch.isChecked
fun buildIdeSetupArguments(): Array<String> {
val args = mutableListOf<String>()
args.setArgument(IdeSetupArgument.INSTALL_DIR, Environment.HOME.absolutePath)
args.setArgument(IdeSetupArgument.SDK_VERSION,
SdkVersion.fromDisplayName(content.sdkVersion.text).version)
args.setArgument(IdeSetupArgument.JDK_VERSION,
JdkVersion.fromDisplayName(content.jdkVersion.text).version)
args.setArgument(IdeSetupArgument.ASSUME_YES)
if (content.withCmdlineTools.isChecked) {
args.setArgument(IdeSetupArgument.WITH_CMDLINE_TOOLS)
}
if (content.installGit.isChecked) {
args.setArgument(IdeSetupArgument.WITH_GIT)
}
if (content.installOpenssh.isChecked) {
args.setArgument(IdeSetupArgument.WITH_OPENSSH)
}
return args.toTypedArray()
}
private fun MutableList<String>.setArgument(argument: IdeSetupArgument, value: Any? = null) {
val strVal = value?.toString() ?: ""
if (argument.requiresValue && strVal.isBlank()) {
throw IllegalArgumentException("${argument.name} requires a value")
}
add(argument.argumentName)
add(strVal)
}
override fun onStart() {
super.onStart()
updateConnectionStatus()
monitorNetworkState()
}
override fun onStop() {
super.onStop()
removeNetworkMonitors()
}
private fun updateConnectionStatus(networkCapabilities: NetworkCapabilities? = null) =
updateConnectionStatus(getConnectionInfo(requireContext(), networkCapabilities))
private fun updateConnectionStatus(connectionInfo: ConnectionInfo) = runOnUiThread {
content.noConnection.root.isVisible = false
content.cellularConnection.root.isVisible = false
content.meteredConnection.root.isVisible = false
content.backgroundDataRestricted.root.isVisible = false
if (connectionInfo === ConnectionInfo.UNKNOWN || !connectionInfo.isConnected) {
showNoConnectionWarning()
return@runOnUiThread
}
if (connectionInfo.isCellularTransport) {
addCellularTransportWarning()
}
if (connectionInfo.isMeteredConnection && !connectionInfo.isCellularTransport) {
addMeteredConnectionWarning()
}
if (connectionInfo.isBackgroundDataRestricted) {
addBackgroundDataRestrictedWarning()
}
}
private fun addBackgroundDataRestrictedWarning() {
content.backgroundDataRestricted.root.apply {
setText(R.string.msg_disable_background_data_restriction)
isVisible = true
}
}
private fun addMeteredConnectionWarning() {
content.meteredConnection.root.apply {
setText(R.string.msg_connected_to_metered_connection)
isVisible = true
}
}
private fun addCellularTransportWarning() {
content.cellularConnection.root.apply {
setText(R.string.msg_connected_to_cellular)
isVisible = true
}
}
private fun showNoConnectionWarning() {
val msg = SpannableStringBuilder(getString(R.string.msg_no_internet))
msg.append(" ")
msg.append(getString(R.string.action_open_settings), URLSpan(""),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
content.noConnection.root.apply {
isVisible = true
setOnClickListener {
it.context.startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
}
}
}
override fun onDestroyView() {
super.onDestroyView()
backgroundDataRestrictionReceiver = null
networkStateChangeCallback = null
}
override val isPolicyRespected: Boolean
get() = getConnectionInfo(requireContext()).isConnected
override fun onUserIllegallyRequestedNextPage() {
flashError(string.msg_no_internet)
}
private fun monitorNetworkState() {
val connectivityManager = requireContext().getSystemService<ConnectivityManager>() ?: return
networkStateChangeCallback?.also {
connectivityManager.registerDefaultNetworkCallback(it)
}
networkStateChangeCallback = object : NetworkCallback() {
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
updateConnectionStatus(networkCapabilities)
}
override fun onLost(network: Network) {
updateConnectionStatus(ConnectionInfo.UNKNOWN)
}
}
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
connectivityManager.registerNetworkCallback(networkRequest, networkStateChangeCallback!!)
backgroundDataRestrictionReceiver?.also {
try {
requireContext().unregisterReceiver(it)
} catch (err: Throwable) { /*ignored*/
}
}
backgroundDataRestrictionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
updateConnectionStatus()
}
}
requireContext().registerReceiver(backgroundDataRestrictionReceiver!!,
IntentFilter(ConnectivityManager.ACTION_RESTRICT_BACKGROUND_CHANGED))
}
private fun removeNetworkMonitors() {
networkStateChangeCallback?.also {
requireContext().getSystemService<ConnectivityManager>()?.unregisterNetworkCallback(it)
networkStateChangeCallback = null
}
backgroundDataRestrictionReceiver?.also {
requireContext().unregisterReceiver(it)
backgroundDataRestrictionReceiver = null
}
}
}
@@ -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.fragments.onboarding
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.view.updateLayoutParams
import com.itsaky.androidide.databinding.FragmentOnboardingBinding
import com.itsaky.androidide.fragments.FragmentWithBinding
open class OnboardingFragment :
FragmentWithBinding<FragmentOnboardingBinding>(FragmentOnboardingBinding::inflate) {
companion object {
const val KEY_ONBOARDING_TITLE = "ide.onboarding.fragment.title"
const val KEY_ONBOARDING_SUBTITLE = "ide.onboarding.fragment.subtitle"
const val KEY_ONBOARDING_EXTRA_INFO = "ide.onboarding.fragment.extraInfo"
@JvmStatic
fun newInstance(title: CharSequence, subtitle: CharSequence,
extraInfo: CharSequence = ""): OnboardingFragment {
return OnboardingFragment().apply {
arguments = Bundle().apply {
putCharSequence(KEY_ONBOARDING_TITLE, title)
putCharSequence(KEY_ONBOARDING_SUBTITLE, subtitle)
putCharSequence(KEY_ONBOARDING_EXTRA_INFO, extraInfo)
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.onboardingTitle.text = requireArguments().getCharSequence(KEY_ONBOARDING_TITLE)
binding.onboardingSubtitle.text = requireArguments().getCharSequence(KEY_ONBOARDING_SUBTITLE)
val extraInfo = requireArguments().getCharSequence(KEY_ONBOARDING_EXTRA_INFO, "")
if (extraInfo.isBlank()) {
binding.onboardingExtraInfo.updateLayoutParams<ViewGroup.LayoutParams> {
height = 0
}
} else {
binding.onboardingExtraInfo.movementMethod = LinkMovementMethodCompat.getInstance()
binding.onboardingExtraInfo.text = extraInfo
}
binding.contentContainer.removeAllViews()
createContentView(binding.contentContainer, true)
}
protected open fun createContentView(parent: ViewGroup, attachToParent: Boolean) {}
}
@@ -0,0 +1,71 @@
/*
* 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.fragments.onboarding
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.widget.ImageViewCompat
import com.itsaky.androidide.databinding.FragmentOnboardingInfoBinding
import com.itsaky.androidide.fragments.FragmentWithBinding
/**
* @author Akash Yadav
*/
class OnboardingInfoFragment :
FragmentWithBinding<FragmentOnboardingInfoBinding>(FragmentOnboardingInfoBinding::inflate) {
companion object {
const val KEY_INFO_TEXT = "ide.onboarding.infoFragment.info"
const val KEY_INFO_ICON = "ide.onboarding.infoFragment.icon"
const val KEY_INFO_ICON_TINT = "ide.onboarding.infoFragment.icon.tint"
@JvmStatic
fun newInstance(
title: CharSequence,
info: CharSequence,
@DrawableRes icon: Int,
@ColorInt iconTint: Int
): OnboardingInfoFragment {
return OnboardingInfoFragment().apply {
arguments = Bundle().apply {
putCharSequence(OnboardingFragment.KEY_ONBOARDING_TITLE, title)
putCharSequence(KEY_INFO_TEXT, info)
putInt(KEY_INFO_ICON, icon)
putInt(KEY_INFO_ICON_TINT, iconTint)
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.infoTitle.text = requireArguments().getCharSequence(
OnboardingFragment.KEY_ONBOARDING_TITLE)
binding.infoMessage.text = requireArguments().getCharSequence(KEY_INFO_TEXT)
binding.image.setImageResource(requireArguments().getInt(KEY_INFO_ICON))
ImageViewCompat.setImageTintList(binding.image,
ColorStateList.valueOf(requireArguments().getInt(
KEY_INFO_ICON_TINT)))
}
}
@@ -0,0 +1,83 @@
/*
* 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.fragments.onboarding
import android.os.Build
import android.os.Bundle
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.adapters.onboarding.DefaultOnboardingItemAdapter
import com.itsaky.androidide.databinding.LayoutOnboardingMultiactionBinding
import com.itsaky.androidide.models.OnboardingItem
import com.itsaky.androidide.utils.uncheckedCast
/**
* @author Akash Yadav
*/
open class OnboardingMultiActionFragment : OnboardingFragment() {
protected var recyclerView: RecyclerView? = null
private set
companion object {
const val KEY_ACTION_ITEMS = "ide.onboarding.multiActionFragment.items"
@JvmStatic
fun newInstance(
title: CharSequence,
subtitle: CharSequence,
items: List<OnboardingItem>
): OnboardingFragment {
return OnboardingMultiActionFragment().apply {
arguments = Bundle().apply {
putCharSequence(KEY_ONBOARDING_TITLE, title)
putCharSequence(KEY_ONBOARDING_SUBTITLE, subtitle)
val arr = items.toTypedArray()
putParcelableArray(KEY_ACTION_ITEMS, arr)
}
}
}
}
override fun createContentView(parent: ViewGroup, attachToParent: Boolean) {
val binding = LayoutOnboardingMultiactionBinding.inflate(layoutInflater, parent, attachToParent)
binding.onboardingItems.adapter = createAdapter()
recyclerView = binding.onboardingItems
}
protected inline fun <reified T : OnboardingItem> getItemsFromArgs(): Array<T> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireArguments().getParcelableArray(KEY_ACTION_ITEMS, T::class.java)!!
} else {
uncheckedCast(
@Suppress("DEPRECATION") requireArguments().getParcelableArray(KEY_ACTION_ITEMS)!!)
}
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView = null
}
protected open fun createAdapter(): RecyclerView.Adapter<*> {
val items = getItemsFromArgs<OnboardingItem>()
return DefaultOnboardingItemAdapter(items.toList())
}
}
@@ -0,0 +1,163 @@
/*
* 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.fragments.onboarding
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.appintro.SlidePolicy
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.onboarding.OnboardingPermissionsAdapter
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.models.OnboardingPermissionItem
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.isAtLeastR
/**
* @author Akash Yadav
*/
class PermissionsFragment : OnboardingMultiActionFragment(), SlidePolicy {
var adapter: OnboardingPermissionsAdapter? = null
private val storagePermissionRequestLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()) {
onPermissionsUpdated()
}
private val settingsTogglePermissionRequestLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
onPermissionsUpdated()
}
private val permissions by lazy {
getRequiredPermissions(requireContext())
}
companion object {
@JvmStatic
fun newInstance(context: Context): PermissionsFragment {
return PermissionsFragment().apply {
arguments = Bundle().apply {
putCharSequence(KEY_ONBOARDING_TITLE,
context.getString(R.string.onboarding_title_permissions))
putCharSequence(KEY_ONBOARDING_SUBTITLE,
context.getString(R.string.onboarding_subtitle_permissions))
}
}
}
@JvmStatic
fun getRequiredPermissions(context: Context): List<OnboardingPermissionItem> {
val permissions = mutableListOf<OnboardingPermissionItem>()
permissions.add(OnboardingPermissionItem(Manifest.permission_group.STORAGE,
R.string.permission_title_storage, R.string.permission_desc_storage,
isStoragePermissionGranted(context)))
permissions.add(OnboardingPermissionItem(Manifest.permission.REQUEST_INSTALL_PACKAGES,
R.string.permission_title_install_packages, R.string.permission_desc_install_packages,
canRequestPackageInstalls(context)))
return permissions
}
@JvmStatic
fun areAllPermissionsGranted(context: Context) : Boolean = getRequiredPermissions(context).all { it.isGranted }
@JvmStatic
fun isStoragePermissionGranted(context: Context): Boolean {
if (isAtLeastR()) {
return Environment.isExternalStorageManager()
}
return checkSelfPermission(context,
Manifest.permission.READ_EXTERNAL_STORAGE) && checkSelfPermission(context,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
@JvmStatic
fun canRequestPackageInstalls(context: Context): Boolean {
return context.packageManager.canRequestPackageInstalls()
}
@JvmStatic
fun isPermissionGranted(context: Context, permission: String): Boolean {
return when (permission) {
Manifest.permission_group.STORAGE -> isStoragePermissionGranted(context)
Manifest.permission.REQUEST_INSTALL_PACKAGES -> context.packageManager.canRequestPackageInstalls()
else -> checkSelfPermission(context, permission)
}
}
@JvmStatic
fun checkSelfPermission(context: Context, permission: String): Boolean {
return ActivityCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
}
}
override fun createAdapter(): RecyclerView.Adapter<*> {
return OnboardingPermissionsAdapter(permissions,
this::requestPermission).also { this.adapter = it }
}
private fun onPermissionsUpdated() {
permissions.forEach { it.isGranted = isPermissionGranted(requireContext(), it.permission) }
recyclerView?.adapter = createAdapter()
}
private fun requestPermission(permission: String) {
when (permission) {
Manifest.permission_group.STORAGE -> requestStoragePermission()
Manifest.permission.REQUEST_INSTALL_PACKAGES -> requestSettingsTogglePermission(
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
}
}
private fun requestStoragePermission() {
if (isAtLeastR()) {
requestSettingsTogglePermission(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
return
}
storagePermissionRequestLauncher.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE))
}
private fun requestSettingsTogglePermission(action: String) {
val intent = Intent(action)
intent.setData(Uri.fromParts("package", BuildInfo.PACKAGE_NAME, null))
settingsTogglePermissionRequestLauncher.launch(intent)
}
override val isPolicyRespected: Boolean
get() = permissions.all { it.isGranted }
override fun onUserIllegallyRequestedNextPage() {
activity?.flashError(R.string.msg_grant_permissions)
}
}
@@ -0,0 +1,43 @@
package com.itsaky.androidide.fragments.onboarding
import android.content.Context
import android.os.Bundle
import android.view.ViewGroup
import com.itsaky.androidide.R
import com.itsaky.androidide.app.IDEApplication
import com.itsaky.androidide.databinding.LayoutOnboardingStatisticsBinding
import com.itsaky.androidide.preferences.internal.statOptIn
class StatisticsFragment : OnboardingFragment() {
private var _content: LayoutOnboardingStatisticsBinding? = null
private val content: LayoutOnboardingStatisticsBinding
get() = checkNotNull(_content) { "Fragment has been destroyed" }
companion object {
@JvmStatic
fun newInstance(context: Context) : StatisticsFragment {
return StatisticsFragment().apply {
arguments = Bundle().apply {
putCharSequence(KEY_ONBOARDING_TITLE, context.getString(R.string.title_androidide_statistics))
putCharSequence(KEY_ONBOARDING_SUBTITLE, context.getString(R.string.idepref_stats_optIn_summary))
}
}
}
}
override fun createContentView(parent: ViewGroup, attachToParent: Boolean) {
_content = LayoutOnboardingStatisticsBinding.inflate(layoutInflater, parent, attachToParent)
content.statOptIn.isChecked = true
}
override fun onDestroy() {
super.onDestroy()
_content = null
}
fun updateStatOptInStatus() {
statOptIn = _content?.statOptIn?.isChecked ?: false
IDEApplication.instance.reportStatsIfNecessary()
}
}
@@ -0,0 +1,74 @@
/*
* 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.fragments.onboarding
import com.itsaky.androidide.app.configuration.CpuArch
private val ARM_ONLY = arrayOf(CpuArch.AARCH64, CpuArch.ARM)
private val ALL = arrayOf(CpuArch.AARCH64, CpuArch.ARM, CpuArch.X86_64)
/**
* Android SDK versions.
*
* @author Akash Yadav
*/
enum class SdkVersion(val version: String, val supportedArchs: Array<CpuArch>) {
SDK_33_0_1("33.0.1", ARM_ONLY),
SDK_34_0_0("34.0.0", ARM_ONLY),
SDK_34_0_1("34.0.1", ARM_ONLY),
SDK_34_0_3("34.0.3", ARM_ONLY),
SDK_34_0_4("34.0.4", ALL),
;
val displayName = "SDK $version"
companion object {
@JvmStatic
fun fromDisplayName(displayName: CharSequence) =
entries.first { it.displayName.contentEquals(displayName) }
@JvmStatic
fun fromVersion(version: CharSequence) = entries.first { it.version.contentEquals(version) }
}
}
/**
* JDK versions.
*
* @author Akash Yadav
*/
enum class JdkVersion(val version: String) {
JDK_17("17"),
JDK_21("21"),
;
val displayName = "JDK $version"
companion object {
@JvmStatic
fun fromDisplayName(displayName: CharSequence) =
entries.first { it.displayName.contentEquals(displayName) }
@JvmStatic
fun fromVersion(version: CharSequence) = entries.first { it.version.contentEquals(version) }
}
}
@@ -0,0 +1,36 @@
/*
* 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.models
import kotlinx.parcelize.Parcelize
/**
* Default implementation of [OnboardingItem].
*
* @author Akash Yadav
*/
@Parcelize
data class DefaultOnboardingItem(
override val key: String,
override val title: CharSequence,
override val description: CharSequence = "",
override val icon: Int = 0,
override val iconTint: Int = 0,
override val isClickable: Boolean = false,
override val isLongClickable: Boolean = false
) : OnboardingItem
@@ -0,0 +1,38 @@
/*
* 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.models
import kotlinx.parcelize.Parcelize
/**
* Implementation of [OnboardingItem] which also contains a link to be opened on click.
*
* @property hyperlink The hyperlink.
* @author Akash Yadav
*/
@Parcelize
data class HyperlinkedOnboardingItem(
override val key: String,
override val title: CharSequence,
val hyperlink: String,
override val description: CharSequence = "",
override val icon: Int = 0,
override val iconTint: Int = 0,
override val isClickable: Boolean = false,
override val isLongClickable: Boolean = false
) : OnboardingItem
@@ -0,0 +1,35 @@
/*
* 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.models
/**
* Configuration options for IDESetup
*
* @property argumentName The name of the argument.
* @property requiresValue Whether the argument must have a value.
* @author Akash Yadav
*/
enum class IdeSetupArgument(val argumentName: String, val requiresValue: Boolean = false) {
INSTALL_DIR("--install-dir", true),
WITH_CMDLINE_TOOLS("--with-cmdline-tools"),
WITH_GIT("--with-git"),
ASSUME_YES("--assume-yes"),
WITH_OPENSSH("--with-openssh"),
SDK_VERSION("--sdk", true),
JDK_VERSION("--jdk", true)
}
@@ -0,0 +1,67 @@
/*
* 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.models
import android.os.Parcelable
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
/**
* Item that is shown on the onboarding fragment.
*
* @author Akash Yadav
*/
interface OnboardingItem : Parcelable {
/**
* The key for the item.
*/
val key: String
/**
* Title for the item.
*/
val title: CharSequence
/**
* Short description (or subtitle) for the item.
*/
val description: CharSequence
/**
* Icon for the item.
*/
@get:DrawableRes
val icon: Int
/**
* Icon tint for the item.
*/
@get:ColorInt
val iconTint: Int
/**
* Whether is item is clickable.
*/
val isClickable: Boolean
/**
* Whether the item is long clickable.
*/
val isLongClickable: Boolean
}
@@ -15,18 +15,20 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.activities
package com.itsaky.androidide.models
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.annotation.StringRes
class SplashActivity : Activity() {
/**
* @author Akash Yadav
*/
data class OnboardingPermissionItem(
val permission: String,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(Intent(this, MainActivity::class.java))
finish()
}
@StringRes
val title: Int,
}
@StringRes
val description: Int,
var isGranted: Boolean
)
@@ -17,15 +17,18 @@
package com.itsaky.androidide.services.builder
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.text.TextUtils
import androidx.core.app.NotificationManagerCompat
import com.blankj.utilcode.util.ResourceUtils
import com.blankj.utilcode.util.ZipUtils
import com.itsaky.androidide.BuildConfig
import com.itsaky.androidide.R.*
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.managers.ToolsManager
@@ -145,9 +148,19 @@ class GradleBuildService : Service(), BuildService, IToolingApiClient,
private fun showNotification(message: String,
@Suppress("SameParameterValue") isProgress: Boolean) {
log.info("Showing notification to user...")
createNotificationChannels()
startForeground(NOTIFICATION_ID, buildNotification(message, isProgress))
}
private fun createNotificationChannels() {
val buildNotificationChannel = NotificationChannel(
BaseApplication.NOTIFICATION_GRADLE_BUILD_SERVICE,
getString(string.title_gradle_service_notification_channel),
NotificationManager.IMPORTANCE_LOW)
NotificationManagerCompat.from(this)
.createNotificationChannel(buildNotificationChannel)
}
private fun buildNotification(message: String, isProgress: Boolean): Notification {
val ticker = getString(R.string.title_gradle_service_notification_ticker)
val title = getString(R.string.title_gradle_service_notification)
@@ -0,0 +1,63 @@
/*
* 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.utils
import android.view.ViewTreeObserver
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager
import kotlin.math.ceil
/**
* @author Akash Yadav
*/
object FlexboxUtils {
@JvmStatic
inline fun <T: RecyclerView.Adapter<*>> createGlobalLayoutListenerToDistributeFlexboxItemsEvenly(
crossinline adapterProvider: () -> T?,
crossinline layoutManagerProvider: () -> FlexboxLayoutManager?,
crossinline fillDiff: (T, diff: Int) -> Unit
): ViewTreeObserver.OnGlobalLayoutListener {
return object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val adapter = adapterProvider() ?: return
val layoutManager = layoutManagerProvider() ?: return
val columns = layoutManager.flexLinesInternal.firstOrNull()?.itemCount ?: 0
if (columns == 0) {
return
}
val itemCount = adapter.itemCount
val rows = ceil(itemCount.toFloat() / columns.toFloat()).toInt()
if (itemCount % columns == 0) {
return
}
val diff = rows * columns - itemCount
if (diff <= 0) {
return
}
fillDiff(adapter, diff)
}
}
}
}
@@ -72,7 +72,7 @@ class MainViewModel : ViewModel() {
fun postTransition(owner: LifecycleOwner, action: Runnable) {
if (isTransitionInProgress) {
_isTransitionInProgress.observe(owner, object : Observer<Boolean> {
override fun onChanged(t: Boolean?) {
override fun onChanged(t: Boolean) {
_isTransitionInProgress.removeObserver(this)
action.run()
}
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<androidx.core.widget.NestedScrollView
android:id="@+id/content_scrollview"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/onboarding_title"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_extra_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
tools:text="@string/greeting_subtitle" />
<LinearLayout
android:id="@+id/content_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/greeting_subtitle"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintEnd_toEndOf="@id/onboarding_title"
app:layout_constraintStart_toStartOf="@id/onboarding_title"
app:layout_constraintTop_toBottomOf="@id/onboarding_title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="@string/greeting_title"
android:textAppearance="@style/TextAppearance.Material3.DisplaySmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/content_scrollview"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/icon"
android:layout_width="0dp"
android:layout_height="150dp"
android:src="@drawable/ic_launcher_fg_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
app:tint="?attr/colorOnSurface" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
android:text="@string/greeting_title"
android:textAppearance="@style/TextAppearance.Material3.DisplayMedium"
app:layout_constraintBottom_toBottomOf="@id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="@id/icon"
app:layout_constraintVertical_bias="0.4" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/greeting_subtitle"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:minHeight="150dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/info_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginTop="16dp"
app:tint="?attr/colorOnSurface"
tools:src="@drawable/ic_error" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/info_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:justificationMode="inter_word"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/image"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/title_unsupported_device" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/info_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:justificationMode="inter_word"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintEnd_toEndOf="@id/info_title"
app:layout_constraintStart_toStartOf="@id/info_title"
app:layout_constraintTop_toBottomOf="@id/info_title"
tools:text="@string/msg_unsupported_device" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<androidx.core.widget.NestedScrollView
android:id="@+id/content_scrollview"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/onboarding_title"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_extra_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
tools:text="@string/greeting_subtitle" />
<LinearLayout
android:id="@+id/content_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/greeting_subtitle"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintEnd_toEndOf="@id/onboarding_title"
app:layout_constraintStart_toStartOf="@id/onboarding_title"
app:layout_constraintTop_toBottomOf="@id/onboarding_title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="@string/greeting_title"
android:textAppearance="@style/TextAppearance.Material3.DisplaySmall"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/content_scrollview"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/icon"
android:layout_width="0dp"
android:layout_height="200dp"
android:src="@drawable/ic_launcher_fg_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
app:tint="?attr/colorOnSurface" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:gravity="center"
android:text="@string/greeting_title"
android:textAppearance="@style/TextAppearance.Material3.DisplayLarge"
app:layout_constraintBottom_toBottomOf="@id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="@id/icon"
app:layout_constraintVertical_bias="0.4" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/greeting_subtitle"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:minHeight="150dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/info_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_goneMarginTop="16dp"
app:tint="?attr/colorOnSurface"
tools:src="@drawable/ic_error" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/info_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:justificationMode="inter_word"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/image"
app:layout_constraintTop_toTopOf="@id/image"
tools:text="@string/title_unsupported_device" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/info_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:justificationMode="inter_word"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintEnd_toEndOf="@id/info_title"
app:layout_constraintStart_toStartOf="@id/info_title"
app:layout_constraintTop_toBottomOf="@id/info_title"
tools:text="@string/msg_unsupported_device" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~r
~ 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/>.
-->
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/onboarding_title"
app:layout_constraintStart_toStartOf="@id/onboarding_title"
app:layout_constraintTop_toBottomOf="@id/onboarding_extra_info" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:layout_constraintEnd_toEndOf="@id/onboarding_title"
app:layout_constraintStart_toStartOf="@id/onboarding_title"
app:layout_constraintTop_toBottomOf="@id/onboarding_title"
tools:text="@string/greeting_subtitle" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_extra_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toEndOf="@id/onboarding_title"
app:layout_constraintStart_toStartOf="@id/onboarding_title"
app:layout_constraintTop_toBottomOf="@id/onboarding_subtitle"
tools:text="@string/greeting_subtitle" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/onboarding_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/greeting_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/icon"
android:layout_width="0dp"
android:layout_height="150dp"
android:src="@drawable/ic_launcher_fg_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Full"
app:tint="?attr/colorOnSurface" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/greeting_title"
android:textAppearance="@style/TextAppearance.Material3.DisplayMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/icon" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/greeting_subtitle"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:minHeight="150dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/info_title"
app:layout_constraintVertical_bias="0.3"
app:layout_goneMarginTop="16dp"
app:tint="?attr/colorOnSurface"
tools:src="@drawable/ic_error" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/info_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:justificationMode="inter_word"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/title_unsupported_device" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/info_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center"
android:justificationMode="inter_word"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/image"
app:layout_constraintVertical_bias="0.4"
tools:text="@string/msg_unsupported_device" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?colorOnSurface"
app:icon="@drawable/ic_info"
app:iconPadding="16dp"
app:iconTint="@color/color_error"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Medium"
app:strokeColor="@color/color_error"
tools:text="Error" />
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical|start"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?colorOnSurface"
app:icon="@drawable/ic_info"
app:iconPadding="16dp"
app:iconTint="@color/color_warning"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Medium"
app:strokeColor="@color/color_warning"
tools:text="Warning" />
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_android"
tools:tint="?attr/colorOnSurface" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="bottom|start"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/greeting_title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title"
tools:text="@string/greeting_subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:clickable="true"
android:focusable="true">
<include
android:id="@+id/content"
layout="@layout/layout_icon_title_subtitle" />
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/onboarding_items"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadingEdge="vertical"
android:fadingEdgeLength="16dp"
android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/layout_onboarding_item" />
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/grant_button"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/title_grant"
app:layout_constraintBottom_toBottomOf="@id/info_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/info_content"
app:layout_constraintTop_toTopOf="@id/info_content" />
<include
android:id="@+id/info_content"
layout="@layout/layout_title_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toStartOf="@id/grant_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.onboarding.OnboardingFragment">
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/stat_opt_in"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:checked="true"
android:text="@string/title_androidide_statistics"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/msg_androidide_statistics"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toEndOf="@id/stat_opt_in"
app:layout_constraintStart_toStartOf="@id/stat_opt_in"
app:layout_constraintTop_toBottomOf="@id/stat_opt_in" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/connection_info_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/auto_install_switch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include
android:id="@+id/no_connection"
layout="@layout/layout_chip_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp" />
<include
android:id="@+id/cellular_connection"
layout="@layout/layout_chip_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp" />
<include
android:id="@+id/metered_connection"
layout="@layout/layout_chip_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp" />
<include
android:id="@+id/background_data_restricted"
layout="@layout/layout_chip_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/with_cmdline_tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:checked="true"
android:enabled="false"
android:text="@string/msg_with_command_line_tools"
app:layout_constraintStart_toStartOf="@id/auto_install_switch"
app:layout_constraintTop_toBottomOf="@id/jdk_version_layout" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/install_git"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/action_install_git"
app:layout_constraintStart_toStartOf="@id/auto_install_switch"
app:layout_constraintTop_toBottomOf="@id/with_cmdline_tools" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/install_openssh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/action_install_openssh"
app:layout_constraintStart_toStartOf="@id/auto_install_switch"
app:layout_constraintTop_toBottomOf="@id/install_git" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/auto_install_switch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:checked="true"
android:text="@string/action_auto_install"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_info_container" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/jdk_version_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/hin_jdk_version"
android:labelFor="@id/jdk_version"
app:layout_constraintEnd_toEndOf="@id/auto_install_switch"
app:layout_constraintStart_toStartOf="@id/auto_install_switch"
app:layout_constraintTop_toBottomOf="@id/sdk_version_layout">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/jdk_version"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/sdk_version_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/hint_android_sdk_version"
android:labelFor="@id/sdk_version"
app:layout_constraintEnd_toEndOf="@id/auto_install_switch"
app:layout_constraintStart_toStartOf="@id/auto_install_switch"
app:layout_constraintTop_toBottomOf="@id/auto_install_switch">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/sdk_version"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="bottom|start"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/greeting_title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title"
tools:text="@string/greeting_subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
+1 -1
View File
@@ -28,7 +28,7 @@ plugins {
id("build-logic.root-project")
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.maven.publish) apply false
alias(libs.plugins.gradle.publish) apply false
}
@@ -61,16 +61,6 @@ public class BaseApplication extends Application {
if (!VMUtils.isJvm()) {
ToolsManager.init(this, null);
}
createNotificationChannels();
}
private void createNotificationChannels() {
NotificationChannel buildNotificationChannel = new NotificationChannel(
NOTIFICATION_GRADLE_BUILD_SERVICE,
getString(R.string.title_gradle_service_notification_channel),
NotificationManager.IMPORTANCE_LOW);
NotificationManagerCompat.from(this).createNotificationChannel(buildNotificationChannel);
}
public void writeException(Throwable th) {
@@ -16,13 +16,9 @@
*/
package com.itsaky.androidide.app
import android.Manifest.permission
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.itsaky.androidide.common.R
@@ -49,13 +45,6 @@ abstract class BaseIDEActivity : AppCompatActivity() {
*/
val activityScope = CoroutineScope(Dispatchers.Default)
val isStoragePermissionGranted: Boolean
get() =
(ContextCompat.checkSelfPermission(this, permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED)
override fun onCreate(savedInstanceState: Bundle?) {
window?.apply {
navigationBarColor = this@BaseIDEActivity.navigationBarColor
@@ -65,15 +54,6 @@ abstract class BaseIDEActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
preSetContentLayout()
setContentView(bindLayout())
if (!isStoragePermissionGranted) {
requestStorage()
return
}
if (isStoragePermissionGranted) {
onStorageAlreadyGranted()
} else {
onStorageDenied()
}
}
override fun onDestroy() {
@@ -101,35 +81,6 @@ abstract class BaseIDEActivity : AppCompatActivity() {
transaction.commit()
}
protected fun requestStorage() {
if (isStoragePermissionGranted) {
onStorageGranted()
return
}
ActivityCompat.requestPermissions(
this,
arrayOf(permission.WRITE_EXTERNAL_STORAGE, permission.READ_EXTERNAL_STORAGE),
REQCODE_STORAGE
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQCODE_STORAGE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
onStorageGranted()
else onStorageDenied()
}
}
protected open fun onStorageGranted() {}
protected open fun onStorageAlreadyGranted() {}
protected open fun onStorageDenied() {}
protected open fun preSetContentLayout() {}
protected abstract fun bindLayout(): View
@@ -16,13 +16,11 @@
*/
package com.itsaky.androidide.utils
import android.app.Activity
import android.content.Context
import android.content.DialogInterface.OnClickListener
import android.view.LayoutInflater
import android.view.View
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.itsaky.androidide.common.R
import com.itsaky.androidide.common.databinding.LayoutDialogProgressBinding
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.resources.R.style
@@ -36,26 +34,6 @@ import java.util.concurrent.atomic.AtomicInteger
*/
object DialogUtils {
@JvmStatic
fun showUnSupportedDevice(context: Activity, buildFlavor: String, deviceArch: String) {
val builder = newMaterialDialogBuilder(context)
builder.setTitle(R.string.title_unsupported_device)
builder.setMessage(context.getString(R.string.msg_unsupported_device, buildFlavor, deviceArch))
builder.setCancelable(false)
builder.setPositiveButton(android.R.string.ok) { _, _ -> context.finishAffinity() }
builder.create().show()
}
@JvmStatic
fun showExperimentalArchSupportWarning(context: Activity, buildFlavor: String, deviceArch: String) {
val builder = newMaterialDialogBuilder(context)
builder.setTitle(R.string.title_experiment_flavor)
builder.setMessage(context.getString(R.string.msg_experimental_flavor, buildFlavor, deviceArch))
builder.setCancelable(false)
builder.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
builder.create().show()
}
@JvmStatic
@JvmOverloads
fun newProgressDialog(
@@ -17,16 +17,22 @@
package com.itsaky.androidide.utils;
import android.annotation.SuppressLint;
import android.system.ErrnoException;
import android.system.Os;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import kotlin.io.FilesKt;
@SuppressLint("SdCardPath")
public final class Environment {
@@ -211,4 +217,19 @@ public final class Environment {
public static File getProjectCacheDir(File projectDir) {
return new File(projectDir, ANDROIDIDE_PROJECT_CACHE_DIR);
}
@NonNull
public static File createTempFile() {
var file = newTempFile();
while (file.exists()) {
file = newTempFile();
}
return file;
}
@NonNull
private static File newTempFile() {
return new File(TMP_DIR, "temp_" + UUID.randomUUID().toString().replace('-', 'X'));
}
}
@@ -0,0 +1,45 @@
/*
* 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.utils
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
/**
* @author Akash Yadav
*/
@ChecksSdkIntAtLeast(Build.VERSION_CODES.P)
fun isAtLeastP() = isAtLeast(Build.VERSION_CODES.P)
@ChecksSdkIntAtLeast(Build.VERSION_CODES.Q)
fun isAtLeastQ() = isAtLeast(Build.VERSION_CODES.Q)
@ChecksSdkIntAtLeast(Build.VERSION_CODES.R)
fun isAtLeastR() = isAtLeast(Build.VERSION_CODES.R)
@ChecksSdkIntAtLeast(Build.VERSION_CODES.S)
fun isAtLeastS() = isAtLeast(Build.VERSION_CODES.S)
@ChecksSdkIntAtLeast(Build.VERSION_CODES.TIRAMISU)
fun isAtLeastT() = isAtLeast(Build.VERSION_CODES.TIRAMISU)
@ChecksSdkIntAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun isAtLeastU() = isAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
fun isAtLeast(version: Int) : Boolean = Build.VERSION.SDK_INT >= version
@@ -0,0 +1,87 @@
/*
* 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.utils
import android.Manifest
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.annotation.RequiresPermission
import androidx.core.content.getSystemService
/**
* Information about the device's internet connection.
*
* @property isConnected Indicates whether the device is able to reach the internet.
* See [NetworkCapabilities.NET_CAPABILITY_INTERNET].
* @property isCellularTransport Indicates whether the device is using a cellular transport.
* See [NetworkCapabilities.TRANSPORT_CELLULAR].
* @property isMeteredConnection Indicates whether the device is using a cellular transport.
* See [NetworkCapabilities.NET_CAPABILITY_NOT_METERED] and [NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED].
* @property isBackgroundDataRestricted Indicates whether the background data is restricted. If the
* application is whitelisted from background data restriction, this will be `false`.
* See [NetworkCapabilities.TRANSPORT_CELLULAR].
* @author Akash Yadav
*/
data class ConnectionInfo(
val isConnected: Boolean,
val isCellularTransport: Boolean,
val isMeteredConnection: Boolean,
val isBackgroundDataRestricted: Boolean
) {
companion object {
@JvmStatic
val UNKNOWN = ConnectionInfo(
isConnected = false,
isCellularTransport = false,
isMeteredConnection = false,
isBackgroundDataRestricted = false
)
}
}
@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
@JvmOverloads
fun getConnectionInfo(context: Context, networkCapabilities: NetworkCapabilities? = null): ConnectionInfo {
val connectivityManager = context.getSystemService<ConnectivityManager>()
?: return ConnectionInfo.UNKNOWN
val activeNetCapabilities = networkCapabilities ?: connectivityManager.activeNetwork?.let(
connectivityManager::getNetworkCapabilities) ?: return ConnectionInfo.UNKNOWN
val isConnected = activeNetCapabilities.hasCapability(
NetworkCapabilities.NET_CAPABILITY_VALIDATED)
val isCellularTransport = activeNetCapabilities.hasTransport(
NetworkCapabilities.TRANSPORT_CELLULAR)
val isMeteredConnection = connectivityManager.isActiveNetworkMetered
// RESTRICT_BACKGROUND_STATUS_DISABLED -> background data usage is not restricted
// RESTRICT_BACKGROUND_STATUS_WHITELISTED -> background data usage might be restricted, but the application is whitelisted
val isBackgroundDataRestricted = !connectivityManager.restrictBackgroundStatus.let {
it == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED
|| it == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED
}
return ConnectionInfo(
isConnected = isConnected,
isCellularTransport = isCellularTransport,
isMeteredConnection = isMeteredConnection,
isBackgroundDataRestricted = isBackgroundDataRestricted
)
}
+3 -1
View File
@@ -15,6 +15,7 @@ antlr4 = "4.13.1"
androidx-work = "2.9.0"
androidx-espresso = "3.5.1"
retrofit = "2.9.0"
appintro = "6.3.1"
markwon = "4.6.2"
maven-publish-plugin = "0.25.3"
@@ -82,6 +83,7 @@ androidx-nav-dynamic_features = { module = "androidx.navigation:navigation-dynam
androidx-tracing = { module = "androidx.tracing:tracing", version = "1.2.0" }
androidx-tracing-ktx = { module = "androidx.tracing:tracing-ktx", version = "1.2.0" }
androidx-viewpager = { module = "androidx.viewpager:viewpager", version = "1.0.0" }
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version = "1.0.0"}
#noinspection GradleDependency
androidx-window-v1alpha9 = { module = "androidx.window:window", version="1.0.0-alpha09" }
androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" }
@@ -147,6 +149,6 @@ maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
maven-publish = { id = "com.vanniktech.maven.publish.base", version.ref = "maven-publish-plugin" }
gradle-publish = { id = "com.gradle.plugin-publish", version = "1.2.1" }
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="#000000"
android:pathData="M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z" />
</vector>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#3DDC84"
android:pathData="M16.61,15.15C16.15,15.15 15.77,14.78 15.77,14.32S16.15,13.5 16.61,13.5H16.61C17.07,13.5 17.45,13.86 17.45,14.32C17.45,14.78 17.07,15.15 16.61,15.15M7.41,15.15C6.95,15.15 6.57,14.78 6.57,14.32C6.57,13.86 6.95,13.5 7.41,13.5H7.41C7.87,13.5 8.24,13.86 8.24,14.32C8.24,14.78 7.87,15.15 7.41,15.15M16.91,10.14L18.58,7.26C18.67,7.09 18.61,6.88 18.45,6.79C18.28,6.69 18.07,6.75 18,6.92L16.29,9.83C14.95,9.22 13.5,8.9 12,8.91C10.47,8.91 9,9.24 7.73,9.82L6.04,6.91C5.95,6.74 5.74,6.68 5.57,6.78C5.4,6.87 5.35,7.08 5.44,7.25L7.1,10.13C4.25,11.69 2.29,14.58 2,18H22C21.72,14.59 19.77,11.7 16.91,10.14H16.91Z" />
</vector>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="#000000"
android:pathData="M10 21H14C14 22.1 13.1 23 12 23S10 22.1 10 21M21 19V20H3V19L5 17V11C5 7.9 7 5.2 10 4.3V4C10 2.9 10.9 2 12 2S14 2.9 14 4V4.3C17 5.2 19 7.9 19 11V17L21 19M17 11C17 8.2 14.8 6 12 6S7 8.2 7 11V18H17V11Z" />
</vector>
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M11.622,24.74s-1.23,0.748 0.855,0.962c2.51,0.32 3.847,0.267 6.625,-0.267a10.02,10.02 0,0 0,1.763 0.855c-6.25,2.672 -14.16,-0.16 -9.244,-1.55zM10.822,21.267s-1.336,1.015 0.748,1.23c2.725,0.267 4.862,0.32 8.55,-0.427a3.26,3.26 0,0 0,1.282 0.801c-7.534,2.244 -15.976,0.214 -10.58,-1.603zM25.569,27.357s0.908,0.748 -1.015,1.336c-3.58,1.07 -15.014,1.39 -18.22,0 -1.122,-0.48 1.015,-1.175 1.7,-1.282 0.695,-0.16 1.07,-0.16 1.07,-0.16 -1.23,-0.855 -8.175,1.763 -3.526,2.51 12.77,2.084 23.296,-0.908 19.983,-2.404zM12.2,17.633s-5.824,1.39 -2.084,1.87c1.603,0.214 4.755,0.16 7.694,-0.053 2.404,-0.214 4.81,-0.64 4.81,-0.64s-0.855,0.374 -1.443,0.748c-5.93,1.55 -17.312,0.855 -14.052,-0.748 2.778,-1.336 5.076,-1.175 5.076,-1.175zM22.62,23.457c5.984,-3.1 3.206,-6.09 1.282,-5.717 -0.48,0.107 -0.695,0.214 -0.695,0.214s0.16,-0.32 0.534,-0.427c3.794,-1.336 6.786,4.007 -1.23,6.09 0,0 0.053,-0.053 0.107,-0.16zM12.79,31.899c5.77,0.374 14.587,-0.214 14.8,-2.94 0,0 -0.427,1.07 -4.755,1.87 -4.916,0.908 -11.007,0.8 -14.587,0.214 0,0 0.748,0.64 4.542,0.855z"
android:fillColor="#4e7896"/>
<path
android:pathData="M18.996,0.001s3.313,3.366 -3.152,8.442c-5.183,4.114 -1.175,6.465 0,9.137 -3.046,-2.725 -5.236,-5.13 -3.74,-7.373C14.294,6.893 20.332,5.3 18.996,0.001zM17.296,15.336c1.55,1.763 -0.427,3.366 -0.427,3.366s3.954,-2.03 2.137,-4.542c-1.656,-2.404 -2.94,-3.58 4.007,-7.587 0,0 -10.953,2.725 -5.717,8.763z"
android:fillColor="#f58219"/>
</vector>
@@ -0,0 +1,26 @@
<!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M480,840q-150,0 -255,-105T120,480q0,-150 105,-255t255,-105q14,0 27.5,1t26.5,3q-41,29 -65.5,75.5T444,300q0,90 63,153t153,63q55,0 101,-24.5t75,-65.5q2,13 3,26.5t1,27.5q0,150 -105,255T480,840ZM480,760q88,0 158,-48.5T740,585q-20,5 -40,8t-40,3q-123,0 -209.5,-86.5T364,300q0,-20 3,-40t8,-40q-78,32 -126.5,102T200,480q0,116 82,198t198,82ZM470,490Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M480,600q50,0 85,-35t35,-85q0,-50 -35,-85t-85,-35q-50,0 -85,35t-35,85q0,50 35,85t85,35ZM480,680q-83,0 -141.5,-58.5T280,480q0,-83 58.5,-141.5T480,280q83,0 141.5,58.5T680,480q0,83 -58.5,141.5T480,680ZM200,520L40,520v-80h160v80ZM920,520L760,520v-80h160v80ZM440,200v-160h80v160h-80ZM440,920v-160h80v160h-80ZM256,310l-101,-97 57,-59 96,100 -52,56ZM748,806 L651,705 704,650 805,747 748,806ZM650,256 L747,155 806,212 706,308 650,256ZM154,748l101,-97 55,53 -97,101 -59,-57ZM480,480Z"/>
</vector>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">حدد تثبيت Gradle مخصص لاستخدامه في البناء. سيقوم بتغيير الإصدار المحدد في gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">مسار تثبيت Gradle</string>
<string name="msg_gradle_installation_input_help">اتركه فارغاً لاستخدام Gradle wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">تحتاج إلى تثبيت JDK و Android SDK حتى يعمل IDE. للتثبيت انقر فوق \'نعم\' واتبع التعليمات <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">هنا</a>]]></string>
<string name="msg_install_tools">تحتاج إلى تثبيت JDK و Android SDK حتى يعمل IDE. للتثبيت انقر فوق \'نعم\' واتبع التعليمات <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">هنا</a>]]></string>
<string name="build_status_idle">لا يوجد بناء قيد التقدم</string>
<string name="build_status_in_progress">البناء جارٍ...</string>
<string name="build_status_failed">فشل البناء. راجع إخراج البناء لمزيد من التفاصيل.</string>
+1 -1
View File
@@ -303,7 +303,7 @@
<string name="idepref_msg_customGradleInstallation">বিল্ডের জন্য ব্যবহার করার জন্য একটি কাস্টম গ্রেডল ইনস্টলেশন নির্দিষ্ট করুন৷ এটি gradle-wrapper.properties-এ উল্লিখিত সংস্করণটিকে ওভাররাইড করে।</string>
<string name="msg_gradle_installation_path">Gradle ইনস্টলেশন পাথ</string>
<string name="msg_gradle_installation_input_help">Gradle wrapper ব্যবহার করতে খালি ছেড়ে দিন</string>
<string name="msg_require_install_jdk_and_android_sdk">IDE কাজ করার জন্য আপনাকে JDK এবং Android SDK ইনস্টল করতে হবে৷ ইনস্টল করতে \'হ্যাঁ\' ক্লিক করুন এবং নির্দেশাবলী অনুসরণ করুন <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">এখানে</a>]]></string>
<string name="msg_install_tools">IDE কাজ করার জন্য আপনাকে JDK এবং Android SDK ইনস্টল করতে হবে৷ ইনস্টল করতে \'হ্যাঁ\' ক্লিক করুন এবং নির্দেশাবলী অনুসরণ করুন <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">এখানে</a>]]></string>
<string name="build_status_idle">কোন বিল্ড প্রক্রিয়াধীন নেই</string>
<string name="build_status_in_progress">বিল্ড চলছে…</string>
<string name="build_status_failed">বিল্ড ব্যর্থ হয়েছে৷ আরো বিস্তারিত জানার জন্য বিল্ড আউটপুট দেখুন।</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">Geben Sie eine benutzerdefinierte Gradle-Installation an, die für den Build verwendet werden soll. Dies ÜBERSCHREIBT die in gradle-wrapper.properties angegebene Version</string>
<string name="msg_gradle_installation_path">Gradle installation Pfad</string>
<string name="msg_gradle_installation_input_help">Lass dies leer um den Gradle wrapper zu benutzen.</string>
<string name="msg_require_install_jdk_and_android_sdk">Sie müssen das JDK und das Android SDK installieren, damit die IDE funktioniert. Um dies zu installieren klick \'Ok\' und folge der Anleitung <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">hier</a>]]></string>
<string name="msg_install_tools">Sie müssen das JDK und das Android SDK installieren, damit die IDE funktioniert. Um dies zu installieren klick \'Ok\' und folge der Anleitung <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">hier</a>]]></string>
<string name="build_status_idle">Kein Build wird ausgeführt</string>
<string name="build_status_in_progress">Build läuft…</string>
<string name="build_status_failed">Build ist fehlgeschlagen. Sieh dir die Ausgabe an für mehr Informationen</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">Especifique una instalación personalizada de Gradle a usar para compilar. Esta versión SÓLO se especifica en gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">Ruta de instalación de Gradle</string>
<string name="msg_gradle_installation_input_help">Dejar en blanco para usar Gradle wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">Necesita instalar el JDK y Android SDK para que el IDE funcione. Para instalar haz clic en \"Yes\" y sigue las instrucciones <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">aquí</a>]]></string>
<string name="msg_install_tools">Necesita instalar el JDK y Android SDK para que el IDE funcione. Para instalar haz clic en \"Yes\" y sigue las instrucciones <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">aquí</a>]]></string>
<string name="build_status_idle">No hay compilaciones en progreso</string>
<string name="build_status_in_progress">Compilación en curso...</string>
<string name="build_status_failed">Error de compilación. Vea la salida de compilación para más detalles.</string>
+1 -1
View File
@@ -303,7 +303,7 @@ Dernier projet ouvert : \n%s</string>
<string name="idepref_msg_customGradleInstallation">Specify a custom Gradle installation to use for build. This OVERRIDES the version specified in gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">Gradle installation path</string>
<string name="msg_gradle_installation_input_help">Leave empty to use Gradle wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">You need to install the JDK and Android SDK for the IDE to work. To install click \'Yes\' and follow the instructions <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">here</a>]]></string>
<string name="msg_install_tools">You need to install the JDK and Android SDK for the IDE to work. To install click \'Yes\' and follow the instructions <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">here</a>]]></string>
<string name="build_status_idle">No builds in progress</string>
<string name="build_status_in_progress">Build in progress…</string>
<string name="build_status_failed">Build failed. See build output for more details.</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">बिल्ड में उपयोग करने के लिए एक कस्टम ग्रेडल इंस्टॉलेशन निर्दिष्ट करें। यह gradle-wrapper.properties में निर्दिष्ट संस्करण को ओवरराइड करता है।</string>
<string name="msg_gradle_installation_path">ग्रेडल इंस्टॉलेशन पथ</string>
<string name="msg_gradle_installation_input_help">ग्रेडल रैपर का उपयोग करने के लिए खाली छोड़ दें।</string>
<string name="msg_require_install_jdk_and_android_sdk">IDE के काम करने के लिए आपको JDK और Android SDK इंस्टॉल करने की आवश्यकता है। इंस्टॉल करने के लिए क्लिक करे \'हां\' और <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">इन निर्देशों का पालन करें</a>]]></string>
<string name="msg_install_tools">IDE के काम करने के लिए आपको JDK और Android SDK इंस्टॉल करने की आवश्यकता है। इंस्टॉल करने के लिए क्लिक करे \'हां\' और <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">इन निर्देशों का पालन करें</a>]]></string>
<string name="build_status_idle">कोई निर्माण प्रगति पर नहीं है</string>
<string name="build_status_in_progress">निर्माण चल रहा है…</string>
<string name="build_status_failed">निर्माण विफल रहा। अधिक विवरण के लिए बिल्ड आउटपुट देखें।</string>
+1 -1
View File
@@ -296,7 +296,7 @@
<string name="idepref_msg_customGradleInstallation">Tentukan penginstalan Custom Gradle yang akan digunakan untuk build. Ini MENGGANTI versi yang ditentukan dalam gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">Path instalasi Gradle</string>
<string name="msg_gradle_installation_input_help">Biarkan kosong untuk menggunakan Gradle wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">Anda perlu menginstal JDK dan Android SDK agar IDE berfungsi. Untuk menginstal klik \'Ya\' dan ikuti petunjuknya <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">di sini</a>]]></string>
<string name="msg_install_tools">Anda perlu menginstal JDK dan Android SDK agar IDE berfungsi. Untuk menginstal klik \'Ya\' dan ikuti petunjuknya <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">di sini</a>]]></string>
<string name="build_status_idle">Tidak ada build yang sedang berlangsung</string>
<string name="build_status_in_progress">Build sedang berlangsung…</string>
<string name="build_status_failed">Build gagal. Lihat output build untuk detail selengkapnya.</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">Tentukan penginstalan Custom Gradle yang akan digunakan untuk build. Ini MENGGANTI versi yang ditentukan dalam gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">Path instalasi Gradle</string>
<string name="msg_gradle_installation_input_help">Biarkan kosong untuk menggunakan Gradle wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">Anda perlu menginstal JDK dan Android SDK agar IDE berfungsi. Untuk menginstal klik \'Ya\' dan ikuti petunjuknya <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">di sini</a>]]></string>
<string name="msg_install_tools">Anda perlu menginstal JDK dan Android SDK agar IDE berfungsi. Untuk menginstal klik \'Ya\' dan ikuti petunjuknya <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">di sini</a>]]></string>
<string name="build_status_idle">Tidak ada build yang sedang berlangsung</string>
<string name="build_status_in_progress">Build sedang berlangsung…</string>
<string name="build_status_failed">Build gagal. Lihat output build untuk detail selengkapnya.</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">Especifique uma instalação personalizada do Gradle a ser usada para compilação. Isso SUBSTITUI a versão especificada em gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">Caminho de instalação do Gradle</string>
<string name="msg_gradle_installation_input_help">Deixe em branco para usar o Gradle Wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">Você precisa instalar o JDK e o Android SDK para que a IDE funcione. Para instalar clique em \'Sim\' e siga as instruções <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">aqui</a>]]></string>
<string name="msg_install_tools">Você precisa instalar o JDK e o Android SDK para que a IDE funcione. Para instalar clique em \'Sim\' e siga as instruções <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">aqui</a>]]></string>
<string name="build_status_idle">Nenhuma compilação em andamento</string>
<string name="build_status_in_progress">Compilação em andamento…</string>
<string name="build_status_failed">Falha na compilação. Consulte a saída de compilação para mais detalhes.</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">Specificați o instalare Gradle particularizată de utilizat pentru compilare. Acest lucru suprascrie versiunea specificată în gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">Calea spre Gradle</string>
<string name="msg_gradle_installation_input_help">Lăsați gol pentru a folosi Gradle wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">Trebuie să instalați SDK-ul JDK și Android pentru ca IDE să funcționeze. Pentru a instala faceți clic pe \'Da\' și urmați instrucțiunile <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">here</a>]]></string>
<string name="msg_install_tools">Trebuie să instalați SDK-ul JDK și Android pentru ca IDE să funcționeze. Pentru a instala faceți clic pe \'Da\' și urmați instrucțiunile <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">here</a>]]></string>
<string name="build_status_idle">Nici o compilare în progres</string>
<string name="build_status_in_progress">Compilare în progres…</string>
<string name="build_status_failed">Compilarea nu a reușit. Consultați Log-ul pentru mai multe detalii.</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">Укажите пользовательскую установку Gradle для использования при сборке. Это ПЕРЕОПРЕДЕЛЯЕТ версию, указанную в gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">Путь установки Gradle</string>
<string name="msg_gradle_installation_input_help">Оставьте пустым, чтобы использовать Gradle wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">Для работы IDE вам необходимо установить JDK и Android SDK. Для установки нажмите \"Да\" и следуйте инструкциям <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">здесь.</a>]]></string>
<string name="msg_install_tools">Для работы IDE вам необходимо установить JDK и Android SDK. Для установки нажмите \"Да\" и следуйте инструкциям <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">здесь.</a>]]></string>
<string name="build_status_idle">Сборки не выполняются</string>
<string name="build_status_in_progress">Сборка в процессе&#8230;</string>
<string name="build_status_failed">Сборка завершилась неудачей. Дополнительные сведения см. в выводе сборки.</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">İnşa için kişisel bir Gradle kurulumu belirleyin. Bu işlem gradle-wrapper.properties dosyasındaki sürümünü GEÇERSİZ KILAR.</string>
<string name="msg_gradle_installation_path">Gradle kurulum yolu</string>
<string name="msg_gradle_installation_input_help">Gradle sarıcı kullanmak için bunu boş bırakın.</string>
<string name="msg_require_install_jdk_and_android_sdk">IDE\'nin çalışması için JDK ve Android SDK yüklemen gerek. Evet\'e tıklayarak anlatılanları takip et<![CDATA[<a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide"> burası</a>]]></string>
<string name="msg_install_tools">IDE\'nin çalışması için JDK ve Android SDK yüklemen gerek. Evet\'e tıklayarak anlatılanları takip et<![CDATA[<a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide"> burası</a>]]></string>
<string name="build_status_idle">Devam eden işlem yok</string>
<string name="build_status_in_progress">Derleme devam ediyor…</string>
<string name="build_status_failed">Yapı başarısız. Daha fazla detay için çıktıya bak.</string>
+1 -1
View File
@@ -302,7 +302,7 @@
<string name="idepref_msg_customGradleInstallation">指定用于构建的自定义 Gradle 路径,这将会覆盖 gradle-wrapper.properties 中指定的版本</string>
<string name="msg_gradle_installation_path">Gradle 安装路径</string>
<string name="msg_gradle_installation_input_help">留空将使用 Gradle 包装器</string>
<string name="msg_require_install_jdk_and_android_sdk">您需要安装 JDK 和 Android SDK 才能使 IDE 工作。要安装,请单击“是”并按照<![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">此处</a>]]>的说明进行操作</string>
<string name="msg_install_tools">您需要安装 JDK 和 Android SDK 才能使 IDE 工作。要安装,请单击“是”并按照<![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">此处</a>]]>的说明进行操作</string>
<string name="build_status_idle">没有正在进行的构建</string>
<string name="build_status_in_progress">构建正在进行中……</string>
<string name="build_status_failed">构建失败,请参阅构建输出以获取详情</string>
+7 -3
View File
@@ -9,10 +9,13 @@
<color name="completionList_backgroundSelected">?attr/colorPrimaryVariant</color>
<color name="rippleHighlightColor">#85121212</color>
<color name="log_warning">#FF7043</color>
<color name="color_error">#f44336</color>
<color name="color_warning">#ff9800</color>
<color name="diagnostic_error">#f44336</color>
<color name="diagnostic_warning">@color/log_warning</color>
<color name="log_warning">@color/color_warning</color>
<color name="diagnostic_error">@color/color_error</color>
<color name="diagnostic_warning">@color/color_warning</color>
<color name="content_background">#212121</color>
@@ -27,5 +30,6 @@
<color name="splash_bg">@android:color/white</color>
<color name="splash_icon_tint">@android:color/black</color>
<color name="color_android">#3DDC84</color>
</resources>
+30 -4
View File
@@ -184,12 +184,9 @@
<string name="classtype_activity">Activity</string>
<string name="title_interface">Interface</string>
<string name="classtype_enum">Enum</string>
<string name="msg_checking_storage_permissions">Checking storage permissions</string>
<string name="msg_failed_list_files">Failed to list project files!</string>
<string name="msg_storage_denied">Can\'t proceed without storage permissions</string>
<!-- Code Editor -->
<string name="msg_storage_granted">Storage permissions granted!</string>
<string name="open_with">Open with...</string>
<string name="undo">Undo</string>
@@ -325,7 +322,6 @@
<string name="idepref_msg_customGradleInstallation">Specify a custom Gradle installation to use for build. This OVERRIDES the version specified in gradle-wrapper.properties.</string>
<string name="msg_gradle_installation_path">Gradle installation path</string>
<string name="msg_gradle_installation_input_help">Leave empty to use Gradle wrapper.</string>
<string name="msg_require_install_jdk_and_android_sdk">You need to install the JDK and Android SDK for the IDE to work. To install click \'Yes\' and follow the instructions <![CDATA[ <a href="https://github.com/itsaky/androidide-build-tools#installing-in-androidide">here</a>]]></string>
<string name="build_status_idle">No builds in progress</string>
<string name="build_status_in_progress">Build in progress…</string>
@@ -624,4 +620,34 @@
<string name="msg_file_is_not_dir">Selected file is not a directory</string>
<string name="msg_project_dir_doesnt_exist">Project directory does not exist</string>
<string name="msg_files_being_saved">A file save operation is already in progress!</string>
<string name="title_install_tools">SDK Installation</string>
<string name="subtitle_install_tools">Install the development tools</string>
<string name="msg_install_tools">The development tools must be installed for the IDE to work. Clicking the done button will open terminal to install the tools. To install, follow the instructions <![CDATA[ <a href="https://androidide.com/blogs/getting-started/2023/03/15/getting-started-with-androidide/">here</a>.<br/><br/>Or read the <a href="https://androidide.com/docs/">documentation</a>]]>.</string>
<string name="title_file_access">File Access Permission</string>
<string name="msg_file_access">This app needs permission to access files to function. Please grant permission to continue.</string>
<string name="greeting_title">Welcome</string>
<string name="greeting_subtitle">Learn, build, launch. All on your Android.</string>
<string name="title_grant">Grant</string>
<string name="permission_title_storage">Storage</string>
<string name="permission_desc_storage">Required to access project files.</string>
<string name="permission_desc_install_packages">Allow installing APKs built with AndroidIDE.</string>
<string name="permission_title_install_packages">Install packages</string>
<string name="permission_desc_notifications">Send notifications regarding terminal and build status.</string>
<string name="permission_title_notifications">Notifications</string>
<string name="onboarding_title_permissions">Permissions</string>
<string name="onboarding_subtitle_permissions">AndroidIDE requires the following permissions</string>
<string name="msg_grant_permissions">Please grant the permissions to continue.</string>
<string name="msg_no_internet">It seems you\'re offline. Check your internet connection.</string>
<string name="msg_connected_to_cellular">Heads up! You\'re using cellular data.</string>
<string name="msg_connected_to_metered_connection">Heads up! You\'re on a metered network.</string>
<string name="msg_disable_background_data_restriction">Disable background data restriction to avoid installation failures.</string>
<string name="msg_cannot_create_terminal_session">Unable to create terminal session</string>
<string name="action_auto_install">Automatic installation</string>
<string name="action_manual_install">Manual installation</string>
<string name="hint_android_sdk_version">Android SDK version</string>
<string name="hin_jdk_version">JDK version</string>
<string name="msg_with_command_line_tools">With command line tools</string>
<string name="action_install_git">Install Git</string>
<string name="action_install_openssh">Install OpenSSH</string>
<string name="msg_terminal_new_sessions_disabled">Cannot create session. New sessions are disabled!</string>
</resources>
+6
View File
@@ -52,6 +52,12 @@
<item name="extraKeysButtonActiveBackgroundColor">@color/grey_500</item>
</style>
<style name="AndroidIDE.Widget.Chip.Warning" parent="Widget.MaterialComponents.Chip.Action">
<item name="chipIcon">@id/info</item>
<item name="chipIconTint">@color/log_warning</item>
<item name="chipStrokeColor">@color/log_warning</item>
</style>
<style
name="TextAppearance.AndroidIDE.TitleLarge.Bold"
parent="TextAppearance.Material3.TitleLarge">
+1
View File
@@ -74,6 +74,7 @@ include(
":lsp:java",
":lsp:xml",
":subprojects:aaptcompiler",
":subprojects:appintro",
":subprojects:builder-model-impl",
":subprojects:flashbar",
":subprojects:framework-stubs",
+1
View File
@@ -0,0 +1 @@
/build
+21
View File
@@ -0,0 +1,21 @@
# AppIntro
Adapted from https://github.com/AppIntro/AppIntro.
# License
```
Copyright (C) 2015-2020 AppIntro Developers
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.
```
+16
View File
@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.github.appintro"
}
dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.annotation)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.viewpager2)
implementation(libs.androidx.fragment.ktx)
}
@@ -0,0 +1 @@
-keep class com.github.appintro.** {*;}
@@ -0,0 +1,202 @@
package com.github.appintro
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.FontRes
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.core.widget.TextViewCompat
import com.github.appintro.internal.TypefaceContainer
abstract class AppIntro : AppIntroBase() {
override val layoutId = R.layout.appintro_intro_layout
/**
* Override viewpager bar color
* @param color your color resource
*/
fun setBarColor(@ColorInt color: Int) {
val bottomBar = findViewById<View>(R.id.bottom)
bottomBar.setBackgroundColor(color)
}
/**
* Override next button arrow color
*
* @param color your color
*/
fun setNextArrowColor(@ColorInt color: Int) {
val nextButton = findViewById<ImageButton>(R.id.next)
nextButton.setColorFilter(color)
}
/**
* Override back button arrow color
*
* @param color your color
*/
fun setBackArrowColor(@ColorInt color: Int) {
val backButton = findViewById<ImageButton>(R.id.back)
backButton.setColorFilter(color)
}
/**
* Override separator color
*
* @param color your color resource
*/
fun setSeparatorColor(@ColorInt color: Int) {
val separator = findViewById<View>(R.id.bottom_separator)
separator.setBackgroundColor(color)
}
/**
* Override skip text
*
* @param text your text
*/
fun setSkipText(text: CharSequence?) {
val skipText = findViewById<TextView>(R.id.skip)
skipText.text = text
}
/**
* Override skip text
*
* @param skipResId your text resource Id
*/
fun setSkipText(@StringRes skipResId: Int) {
val skipText = findViewById<TextView>(R.id.skip)
skipText.setText(skipResId)
}
/**
* Override skip text typeface
*
* @param typeface the typeface to apply to Skip button
*/
fun setSkipTextTypeface(@FontRes typeface: Int) {
val view = findViewById<TextView>(R.id.skip)
TypefaceContainer(null, typeface).applyTo(view)
}
/**
* Override skip text typeface
*
* @param typeURL URL of font file located in Assets folder
*/
fun setSkipTextTypeface(typeURL: String?) {
val view = findViewById<TextView>(R.id.skip)
TypefaceContainer(typeURL, 0).applyTo(view)
}
/**
* Override done text
*
* @param text your text
*/
fun setDoneText(text: CharSequence?) {
val doneText = findViewById<TextView>(R.id.done)
doneText.text = text
}
/**
* Override done text
*
* @param doneResId your text resource Id
*/
fun setDoneText(@StringRes doneResId: Int) {
val doneText = findViewById<TextView>(R.id.done)
doneText.setText(doneResId)
}
/**
* Override done text typeface
*
* @param typeURL URL of font file located in Assets folder
*/
fun setDoneTextTypeface(typeURL: String?) {
val view = findViewById<TextView>(R.id.done)
TypefaceContainer(typeURL, 0).applyTo(view)
}
/**
* Override done text typeface
*
* @param typeface the typeface to apply to Done button
*/
fun setDoneTextTypeface(@FontRes typeface: Int) {
val view = findViewById<TextView>(R.id.done)
TypefaceContainer(null, typeface).applyTo(view)
}
/**
* Override done button text color
*
* @param colorDoneText your color resource
*/
fun setColorDoneText(@ColorInt colorDoneText: Int) {
val doneText = findViewById<TextView>(R.id.done)
doneText.setTextColor(colorDoneText)
}
/**
* Override done button text overall style
*
* @param textAppearance your text style from resource
*/
fun setDoneTextAppearance(@StyleRes textAppearance: Int) {
val doneText = findViewById<TextView>(R.id.done)
TextViewCompat.setTextAppearance(doneText, textAppearance)
}
/**
* Override skip button color
*
* @param colorSkipButton your color resource
*/
fun setColorSkipButton(@ColorInt colorSkipButton: Int) {
val skip = findViewById<TextView>(R.id.skip)
skip.setTextColor(colorSkipButton)
}
/**
* Override skip button text overall style
*
* @param textAppearance your text style from resource
*/
fun setSkipTextAppearance(@StyleRes textAppearance: Int) {
val skip = findViewById<TextView>(R.id.skip)
TextViewCompat.setTextAppearance(skip, textAppearance)
}
/**
* Override Next button
*
* @param imageNextButton your drawable resource
*/
fun setImageNextButton(imageNextButton: Drawable) {
val nextButton = findViewById<ImageView>(R.id.next)
nextButton.setImageDrawable(imageNextButton)
}
/**
* Show or hide the Separator line.
* This is a static setting and Separator state is maintained across slides
* until explicitly changed.
*
* @param showSeparator Set : true to display. false to hide.
*/
fun showSeparator(showSeparator: Boolean) {
val bottomSeparator = findViewById<View>(R.id.bottom_separator)
if (showSeparator) {
bottomSeparator.visibility = View.VISIBLE
} else {
bottomSeparator.visibility = View.INVISIBLE
}
}
}
@@ -0,0 +1,94 @@
package com.github.appintro
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.constraintlayout.widget.ConstraintLayout
abstract class AppIntro2 : AppIntroBase() {
override val layoutId = R.layout.appintro_intro_layout2
@DrawableRes
var backgroundResource: Int? = null
set(value) {
field = value
if (field != null) {
field?.let { backgroundFrame.setBackgroundResource(it) }
}
}
var backgroundDrawable: Drawable? = null
set(value) {
field = value
if (field != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
backgroundFrame.background = field
}
}
}
private lateinit var backgroundFrame: ConstraintLayout
private lateinit var bottomBar: View
private lateinit var skipImageButton: ImageButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
backgroundFrame = findViewById(R.id.background)
bottomBar = findViewById(R.id.bottom)
skipImageButton = findViewById(R.id.skip)
if (isRtl) {
skipImageButton.scaleX = -1F
}
}
/**
* Override viewpager bar color
* @param color your color resource
*/
fun setBarColor(@ColorInt color: Int) {
bottomBar.setBackgroundColor(color)
}
/**
* Override Skip button drawable
* @param imageSkipButton your drawable resource
*/
fun setImageSkipButton(imageSkipButton: Drawable) {
skipImageButton.setImageDrawable(imageSkipButton)
}
/**
* Override next button arrow color
*
* @param color your color
*/
fun setNextArrowColor(@ColorInt color: Int) {
val nextButton = findViewById<ImageButton>(R.id.next)
nextButton.setColorFilter(color)
}
/**
* Override skip button color
*
* @param colorSkipButton your color resource
*/
fun setSkipArrowColor(@ColorInt colorSkipButton: Int) {
val skip = findViewById<ImageButton>(R.id.skip)
skip.setColorFilter(colorSkipButton)
}
/**
* Override done button drawable resource
*
* @param imageDoneButton your drawable resource
*/
fun setImageDoneButton(imageDoneButton: Drawable) {
val done = findViewById<ImageButton>(R.id.done)
done.setImageDrawable(imageDoneButton)
}
}
@@ -0,0 +1,842 @@
@file:Suppress("TooManyFunctions")
package com.github.appintro
import android.animation.ArgbEvaluator
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.ImageButton
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.TooltipCompat.setTooltipText
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat.Type.systemBars
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import androidx.viewpager2.widget.ViewPager2
import com.github.appintro.indicator.DotIndicatorController
import com.github.appintro.indicator.IndicatorController
import com.github.appintro.indicator.ProgressIndicatorController
import com.github.appintro.internal.AppIntroViewPagerController
import com.github.appintro.internal.LayoutUtil
import com.github.appintro.internal.LogHelper
import com.github.appintro.internal.PermissionWrapper
import com.github.appintro.internal.VibrationHelper
import com.github.appintro.internal.viewpager.PagerAdapter
import com.github.appintro.internal.viewpager.ViewPagerTransformer
/**
* The AppIntro Base Class. This class is the Activity that is responsible of handling
* the lifecycle and all the event callbacks for AppIntro.
*/
abstract class AppIntroBase : AppCompatActivity(), AppIntroViewPagerListener {
/** The layout ID that will be used during inflation. */
@get:LayoutRes
protected abstract val layoutId: Int
/**
* A subclass of [IndicatorController] to show the progress.
* If not provided will be initialized with a [DotIndicatorController] */
protected var indicatorController: IndicatorController? = null
/** Toggles all the buttons visibility (between visible and invisible). */
protected var isButtonsEnabled: Boolean = true
set(value) {
field = value
updateButtonsVisibility()
}
/** Toggles only the SKIP button visibility (allows the user to skip the Intro). */
protected var isSkipButtonEnabled = true
set(value) {
field = value
updateButtonsVisibility()
}
/** Toggles the Wizard mode (back button instead of skip). */
protected var isWizardMode: Boolean = false
set(value) {
field = value
this.isSkipButtonEnabled = !value
updateButtonsVisibility()
}
/** Toggles the [IndicatorController] visibility. */
protected var isIndicatorEnabled = true
set(value) {
field = value
indicatorContainer.isVisible = value
}
/**
* Toggles leaving the device's software/hardware back button.
* If set to true, the device's back button will be ignored.
* Note, that does does NOT lock swiping back through the slides.
*/
protected var isSystemBackButtonLocked = false
/** Toggles color cross-fading between slides.
* Please note that slides should implement [SlideBackgroundColorHolder] */
protected var isColorTransitionsEnabled = false
/** Vibration duration in milliseconds */
protected var vibrateDuration = DEFAULT_VIBRATE_DURATION
/**
* Toggles vibration on button clicks If you enable it, don't forget to grant
* vibration permissions on your AndroidManifest.xml file
* */
protected var isVibrate = false
/**
* Read-only property with the total number of slides for this AppIntro.
*/
protected val totalSlidesNumber: Int get() = slidesNumber
// Private Fields
private lateinit var pagerAdapter: PagerAdapter
private lateinit var pagerController: AppIntroViewPagerController
private var slidesNumber: Int = 0
private var savedCurrentItem: Int = 0
private var currentlySelectedItem = -1
private val fragments: MutableList<Fragment> = mutableListOf()
private lateinit var nextButton: View
private lateinit var doneButton: View
private lateinit var skipButton: View
private lateinit var backButton: View
private lateinit var indicatorContainer: ViewGroup
// Asks the ViewPager for the current slide number. Useful to query the [permissionsMap]
private val currentSlideNumber: Int
get() = pagerController.getCurrentSlideNumber(fragments.size)
/** HashMap that contains the [PermissionWrapper] objects */
private var permissionsMap = HashMap<Int, PermissionWrapper>()
private var retainIsButtonsEnabled = true
private val argbEvaluator = ArgbEvaluator()
internal val isRtl: Boolean
get() = LayoutUtil.isRtl(this)
/*
PUBLIC API
=================================== */
/**
* Adds a new slide at the end of the Intro
* @param fragment Instance of Fragment which should be added as slide.
*/
protected fun addSlide(fragment: Fragment) {
if (isRtl) {
fragments.add(0, fragment)
pagerAdapter.notifyItemInserted(0)
} else {
fragments.add(fragment)
pagerAdapter.notifyItemInserted(fragments.size)
}
if (isWizardMode) {
pagerController.setOffscreenPageLimit(fragments.size)
}
}
/**
* Called by the user to associate permissions with slides.
*
* @param permissions - Array of permissions
* @param slideNumber - The slide at which permission is to be asked.
* @param required - Whether the user can change this slide without granting the permissions.
*/
@JvmOverloads
protected fun askForPermissions(permissions: Array<String>, slideNumber: Int, required: Boolean = true) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (slideNumber <= 0) {
error("Invalid Slide Number: $slideNumber")
} else {
permissionsMap[slideNumber] = PermissionWrapper(permissions, slideNumber, required)
}
}
}
/** Moves the AppIntro to the previous slide */
protected fun goToPreviousSlide() {
pagerController.goToPreviousSlide()
}
/** Moves the AppIntro to the next slide */
@JvmOverloads
protected fun goToNextSlide(isLastSlide: Boolean = pagerController.isLastSlide(fragments.size)) {
if (isLastSlide) {
onIntroFinished()
} else {
pagerController.goToNextSlide()
onNextSlide()
}
}
/** Enable the Immersive Sticky Mode */
protected fun setImmersiveMode() {
WindowCompat.getInsetsController(window, window.decorView).apply {
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
hide(systemBars())
}
}
/** Customize the color of the Status Bar */
protected fun setStatusBarColor(@ColorInt color: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// We set the light status bar/translucent first via the WindowInsetsControllerCompat
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true
window.statusBarColor = color
}
}
/** Customize the color of the Status Bar */
protected fun setStatusBarColorRes(@ColorRes color: Int) {
setStatusBarColor(ContextCompat.getColor(this, color))
}
/** Customize the color of the Navigation Bar */
protected fun setNavBarColor(@ColorInt color: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.navigationBarColor = color
}
}
/** Customize the color of the Navigation Bar */
protected fun setNavBarColorRes(@ColorRes color: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.navigationBarColor = ContextCompat.getColor(this, color)
}
}
/** Toggle the Status Bar visibility */
protected fun showStatusBar(show: Boolean) {
val controller = WindowCompat.getInsetsController(window, window.decorView)
if (show) {
controller.show(systemBars())
} else {
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE
controller.hide(systemBars())
}
}
/**
* @param lock Set true to disable forward swiping. False to enable.
* @deprecated setNextPageSwipeLock has been deprecated in favor of setSwipeLock or SlidePolicy
*/
@Deprecated(
"setNextPageSwipeLock has been deprecated in favor of setSwipeLock or SlidePolicy",
ReplaceWith("setSwipeLock"),
DeprecationLevel.ERROR
)
@Suppress("UnusedPrivateMember", "UNUSED_PARAMETER")
protected fun setNextPageSwipeLock(lock: Boolean) {
LogHelper.w(
TAG,
"Calling setNextPageSwipeLock has not effect here. Please switch to setSwipeLock or SlidePolicy"
)
}
/**
* Setting to disable swiping left and right on current page. This also
* hides/shows the Next and Done buttons accordingly.
*
* @param lock Set true to disable forward swiping. False to enable.
*/
protected fun setSwipeLock(lock: Boolean) {
// We retain the button state in order to be able to restore
// it properly afterwards.
if (lock) {
retainIsButtonsEnabled = this.isButtonsEnabled
this.isButtonsEnabled = true
} else {
this.isButtonsEnabled = retainIsButtonsEnabled
}
pagerController.isFullPagingEnabled = !lock
}
/**
* Set a [ProgressIndicatorController] instead of dots (a [DotIndicatorController].
* This is recommended for a large amount of slides. In this case there
* could not be enough space to display all dots on smaller device screens.
*/
protected fun setProgressIndicator() {
indicatorController = ProgressIndicatorController(this)
}
/**
* Overrides color of selected and unselected indicator colors
* @param selectedIndicatorColor your selected color
* @param unselectedIndicatorColor your unselected color
*/
protected fun setIndicatorColor(
@ColorInt selectedIndicatorColor: Int,
@ColorInt unselectedIndicatorColor: Int
) {
indicatorController?.selectedIndicatorColor = selectedIndicatorColor
indicatorController?.unselectedIndicatorColor = unselectedIndicatorColor
}
/*
TRANSFORMERS
=================================== */
/** Allows to specify one of the [AppIntroPageTransformerType] for the ViewPager */
protected fun setTransformer(appIntroTransformer: AppIntroPageTransformerType) {
pagerController.setAppIntroPageTransformer(appIntroTransformer)
}
/** Overrides viewpager transformer with you custom [ViewPagerTransformer] */
protected fun setCustomTransformer(transformer: ViewPager2.PageTransformer?) {
pagerController.setPageTransformer(transformer)
}
/*
CALLBACKS
=================================== */
/**
* Called by [AppIntroViewPager] when the user swipes forward on a slide which contains permissions.
* [.setSwipeLock] is called to prevent user from swiping while permission is requested.
* [.checkAndRequestPermissions] is called to request permissions from the user.
*/
override fun onUserRequestedPermissionsDialog() {
LogHelper.d(TAG, "Requesting Permissions on $currentSlideNumber")
requestPermissions()
}
/**
* Called when the user checks "Don't ask again" in the permission dialog
* or when the permission is disabled by the device policy.
*
* @param permissionName - Name of the permission disabled.
*/
protected open fun onUserDisabledPermission(permissionName: String) {}
/**
* Called when the user denies a permission to the app.
* This method is called both when the permission was required (so we
* block the user) and when it was not (so the user can actually proceed).
*
* @param permissionName - Name of the permission denied.
*/
protected open fun onUserDeniedPermission(permissionName: String) {}
/**
* Called after a new slide has been selected
* @param position Position of the new selected slide
*/
protected open fun onPageSelected(position: Int) {}
/** Called when the user clicked the done button */
protected open fun onDonePressed(currentFragment: Fragment?) {}
/** Called when the user clicked the next button */
protected open fun onNextPressed(currentFragment: Fragment?) {}
/** Called when the user clicked the skip button */
protected open fun onSkipPressed(currentFragment: Fragment?) {}
/** Called when the user request to go to the next slide either
* via keyboard (Enter, etc.) or via button */
protected open fun onNextSlide() {}
/** Called when the AppIntro reached the end. */
protected open fun onIntroFinished() {}
/**
* Called when the selected fragment changed.
* @param oldFragment Instance of the fragment which was displayed before.
* This might be null if the the intro has just started.
* @param newFragment Instance of the fragment which is displayed now.
* This might be null if the intro has finished
*/
protected open fun onSlideChanged(oldFragment: Fragment?, newFragment: Fragment?) {}
/*
LIFECYCLE
=================================== */
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
super.onCreate(savedInstanceState)
// Add back handler
addBackHandler()
// We default the indicator controller to the Dotted one.
indicatorController = DotIndicatorController(this)
// We default to don't show the Status Bar. User can override this.
showStatusBar(false)
setContentView(layoutId)
indicatorContainer = findViewById(R.id.indicator_container)
?: error("Missing Indicator Container: R.id.indicator_container")
nextButton = findViewById(R.id.next) ?: error("Missing Next button: R.id.next")
doneButton = findViewById(R.id.done) ?: error("Missing Done button: R.id.done")
skipButton = findViewById(R.id.skip) ?: error("Missing Skip button: R.id.skip")
backButton = findViewById(R.id.back) ?: error("Missing Back button: R.id.back")
setTooltipText(nextButton, getString(R.string.app_intro_next_button))
if (skipButton is ImageButton) {
setTooltipText(skipButton, getString(R.string.app_intro_skip_button))
}
if (doneButton is ImageButton) {
setTooltipText(doneButton, getString(R.string.app_intro_done_button))
}
if (backButton is ImageButton) {
setTooltipText(backButton, getString(R.string.app_intro_back_button))
}
if (isRtl) {
nextButton.scaleX = -1f
backButton.scaleX = -1f
}
pagerAdapter = PagerAdapter(this@AppIntroBase, fragments)
pagerController = AppIntroViewPagerController(
viewPager = findViewById(R.id.view_pager),
viewPagerGestureOverlay = findViewById(R.id.pager_gesture_overlay)
)
doneButton.setOnClickListener(NextSlideOnClickListener(isLastSlide = true))
nextButton.setOnClickListener(NextSlideOnClickListener(isLastSlide = false))
backButton.setOnClickListener { pagerController.goToPreviousSlide() }
skipButton.setOnClickListener {
dispatchVibration()
onSkipPressed(getPagerItem(pagerController.getCurrentItem()))
}
pagerController.setAdapter(this.pagerAdapter)
pagerController.registerOnPageChangeCallback(OnPageChangeCallback())
pagerController.onNextPageRequestedListener = this
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
slidesNumber = fragments.size
initializeIndicator()
// Makes sure we correctly retain the `isButtonsEnabled` just after onCreate
retainIsButtonsEnabled = isButtonsEnabled
// Required for triggering onPageSelected and onSlideChanged for the first page.
if (isRtl) {
pagerController.setCurrentViewPagerItem(fragments.size - savedCurrentItem)
} else {
pagerController.setCurrentViewPagerItem(savedCurrentItem)
}
pagerController.post {
if (pagerController.getCurrentItem() < pagerAdapter.itemCount) {
dispatchSlideChangedCallbacks(
null,
getPagerItem(pagerController.getCurrentItem())
)
} else {
// Close the intro if there are no slides to show
finish()
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.apply {
putInt(ARG_BUNDLE_SLIDES_NUMBER, slidesNumber)
putBoolean(ARG_BUNDLE_RETAIN_IS_BUTTONS_ENABLED, retainIsButtonsEnabled)
putBoolean(ARG_BUNDLE_IS_BUTTONS_ENABLED, isButtonsEnabled)
putBoolean(ARG_BUNDLE_IS_SKIP_BUTTON_ENABLED, isSkipButtonEnabled)
putBoolean(ARG_BUNDLE_IS_INDICATOR_ENABLED, isIndicatorEnabled)
// We can't use pager.currentItem as we need the current item that is RTL-invariant.
val currentItem = if (isRtl) {
fragments.size - pagerController.getCurrentItem()
} else {
pagerController.getCurrentItem()
}
putInt(ARG_BUNDLE_CURRENT_ITEM, currentItem)
putBoolean(ARG_BUNDLE_IS_FULL_PAGING_ENABLED, pagerController.isFullPagingEnabled)
putSerializable(ARG_BUNDLE_PERMISSION_MAP, permissionsMap)
putBoolean(ARG_BUNDLE_COLOR_TRANSITIONS_ENABLED, isColorTransitionsEnabled)
}
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
with(savedInstanceState) {
slidesNumber = getInt(ARG_BUNDLE_SLIDES_NUMBER)
retainIsButtonsEnabled = getBoolean(ARG_BUNDLE_RETAIN_IS_BUTTONS_ENABLED)
isButtonsEnabled = getBoolean(ARG_BUNDLE_IS_BUTTONS_ENABLED)
isSkipButtonEnabled = getBoolean(ARG_BUNDLE_IS_SKIP_BUTTON_ENABLED)
isIndicatorEnabled = getBoolean(ARG_BUNDLE_IS_INDICATOR_ENABLED)
savedCurrentItem = getInt(ARG_BUNDLE_CURRENT_ITEM)
pagerController.isFullPagingEnabled = getBoolean(ARG_BUNDLE_IS_FULL_PAGING_ENABLED)
@Suppress("UNCHECKED_CAST", "DEPRECATION")
permissionsMap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
(getSerializable(ARG_BUNDLE_PERMISSION_MAP, HashMap::class.java) as HashMap<Int, PermissionWrapper>?)
?: hashMapOf()
} else {
(getSerializable(ARG_BUNDLE_PERMISSION_MAP) as HashMap<Int, PermissionWrapper>?)
?: hashMapOf()
}
isColorTransitionsEnabled = getBoolean(ARG_BUNDLE_COLOR_TRANSITIONS_ENABLED)
}
}
private fun initializeIndicator() {
indicatorContainer.addView(indicatorController?.newInstance(this))
indicatorController?.initialize(slidesNumber)
indicatorController?.selectPosition(currentlySelectedItem)
}
override fun onKeyDown(code: Int, event: KeyEvent): Boolean {
// Handle the navigation with 'Enter' or Dpad events.
if (code == KeyEvent.KEYCODE_ENTER ||
code == KeyEvent.KEYCODE_BUTTON_A ||
code == KeyEvent.KEYCODE_DPAD_CENTER
) {
val isLastSlide = pagerController.isLastSlide(fragments.size)
goToNextSlide(isLastSlide)
if (isLastSlide) {
// We emulate the onDonePressed here to keep backward compatibility
// with the previous API (users expect an onDonePressed to kill the Activity).
// Ideally we should get rid of this extra callback in one of the future release.
onDonePressed(getPagerItem(pagerController.getCurrentItem()))
}
return false
}
return super.onKeyDown(code, event)
}
/*
BACK HANDLER
=================================== */
private fun addBackHandler() {
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// Do nothing if go back lock is enabled or slide has custom policy.
if (isSystemBackButtonLocked) {
return
}
if (pagerController.isFirstSlide(fragments.size)) {
finish()
} else {
pagerController.goToPreviousSlide()
}
}
}
)
}
/*
BUTTONS VISIBILITY FLAGS
=================================== */
private fun updateButtonsVisibility() {
if (isButtonsEnabled) {
val isLastSlide = pagerController.isLastSlide(fragments.size)
val isFirstSlide = pagerController.isFirstSlide(fragments.size)
nextButton.isVisible = !isLastSlide
doneButton.isVisible = isLastSlide
skipButton.isVisible = isSkipButtonEnabled && !isLastSlide
backButton.isVisible = isWizardMode && !isFirstSlide
} else {
nextButton.isVisible = false
doneButton.isVisible = false
backButton.isVisible = false
skipButton.isVisible = false
}
}
/*
SLIDE POLICY
=================================== */
/**
* Called before a slide change happens. By returning false, one can
* disallow the slide change.
*
* @return true, if the slide change should be allowed, else false
*/
override fun onCanRequestNextPage(): Boolean {
val currentFragment = getPagerItem(pagerController.getCurrentItem())
// Check if the current fragment implements SlidePolicy, else a change is always allowed.
return if (currentFragment is SlidePolicy && !currentFragment.isPolicyRespected) {
LogHelper.d(TAG, "Slide policy not respected, denying change request.")
false
} else {
LogHelper.d(TAG, "Change request will be allowed.")
true
}
}
override fun onIllegallyRequestedNextPage() {
val currentFragment = getPagerItem(pagerController.getCurrentItem())
if (currentFragment is SlidePolicy) {
if (!currentFragment.isPolicyRespected) {
currentFragment.onUserIllegallyRequestedNextPage()
}
}
}
/*
PERMISSION
=================================== */
private fun shouldRequestPermission() = permissionsMap.containsKey(currentSlideNumber)
// Request one or more permission to the Android SDK.
private fun requestPermissions() {
setSwipeLock(true)
val permissionToRequest = permissionsMap[currentSlideNumber]
permissionToRequest?.let {
ActivityCompat.requestPermissions(
this,
it.permissions,
PERMISSIONS_REQUEST_ALL_PERMISSIONS
)
}
}
/**
* Handles the Permission Result from Android SDK.
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
setSwipeLock(false)
if (requestCode != PERMISSIONS_REQUEST_ALL_PERMISSIONS) {
return
}
val deniedPermissions = grantResults
.mapIndexed { index, result -> (permissions[index] to result) }
.filter { (_, result) -> result == PackageManager.PERMISSION_DENIED }
.map { (permission, _) -> permission }
// Check if all permissions are granted.
if (deniedPermissions.isEmpty()) {
// If the permission request was a success, we remove
// the permission from the permissionsMap.
permissionsMap.remove(currentSlideNumber)
goToNextSlide()
} else {
// At least one or all of the permissions have been denied.
deniedPermissions.forEach(::handleDeniedPermission)
// Let's force a recenter of the current slide.
pagerController.reCenterCurrentSlide()
}
}
/**
* Function to supports the event dispatching and the handling of the
* various "Permission Denied" scenarios.
*/
private fun handleDeniedPermission(permission: String) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
// Permission is denied for the first time (never ask again box is not checked).
// Ask again explaining the usage of the permission (Show an AlertDialog or Snackbar)
onUserDeniedPermission(permission)
} else {
// Permission is disabled (never ask again is checked)
// Ask the user to go to settings to enable permission.
onUserDisabledPermission(permission)
}
// If the permission was not required, we can remove it from the App and let the user proceed.
permissionsMap[currentSlideNumber]?.let { requestedPermission ->
if (!requestedPermission.required) {
permissionsMap.remove(requestedPermission.position)
goToNextSlide()
}
}
}
private fun dispatchVibration() {
if (isVibrate) {
VibrationHelper.vibrate(this, vibrateDuration)
}
}
/**
* Getter used to notify [AppIntroViewPager] if the currently selected slide
* has permissions attached to it.
*/
private val isPermissionSlide: Boolean
get() = pagerController.getCurrentSlideNumber(fragments.size) in permissionsMap
/** Takes care of calling all the necessary callbacks on Slide Changing. */
private fun dispatchSlideChangedCallbacks(oldFragment: Fragment?, newFragment: Fragment?) {
if (oldFragment is SlideSelectionListener) {
oldFragment.onSlideDeselected()
}
if (newFragment is SlideSelectionListener) {
newFragment.onSlideSelected()
}
onSlideChanged(oldFragment, newFragment)
}
/** Performs color interpolation between two slides.. */
private fun performColorTransition(currentSlide: Fragment?, nextSlide: Fragment?, positionOffset: Float) {
if (nextSlide == null) return
if (currentSlide is SlideBackgroundColorHolder &&
nextSlide is SlideBackgroundColorHolder
) {
// Check if both fragments are attached to an activity,
// otherwise getDefaultBackgroundColor may fail.
if (currentSlide.isAdded && nextSlide.isAdded) {
val newColor = argbEvaluator.evaluate(
positionOffset,
getSlideColor(currentSlide),
getSlideColor(nextSlide)
) as Int
currentSlide.setBackgroundColor(newColor)
nextSlide.setBackgroundColor(newColor)
}
} else {
error("Color transitions are only available if all slides implement SlideBackgroundColorHolder.")
}
}
@ColorInt
@Suppress("DEPRECATION")
private fun getSlideColor(slide: SlideBackgroundColorHolder): Int {
if (slide.defaultBackgroundColorRes != 0) {
return ContextCompat.getColor(this, slide.defaultBackgroundColorRes)
}
return slide.defaultBackgroundColor
}
private fun getPagerItem(position: Int): Fragment? = pagerAdapter.getItem(position, supportFragmentManager)
/**
* Onclick listener for the Next/Done button.
* @param isLastSlide True if you're using this for the DONE button.
*/
private inner class NextSlideOnClickListener(var isLastSlide: Boolean) : View.OnClickListener {
override fun onClick(view: View) {
dispatchVibration()
// Check if changing to the next slide is allowed
if (!onCanRequestNextPage()) {
onIllegallyRequestedNextPage()
return
}
if (shouldRequestPermission()) {
requestPermissions()
return
}
// We can successfully change slide, let's do it.
val currentFragment = getPagerItem(pagerController.getCurrentItem())
if (isLastSlide) {
onDonePressed(currentFragment)
} else {
onNextPressed(currentFragment)
}
goToNextSlide(isLastSlide)
}
}
/**
* [OnPageChangeCallback] used to handle all the callbacks coming from the ViewPager.
* Moreover, if [isColorTransitionsEnabled] a color interpolation will happen in the [onPageScrolled]
*/
internal inner class OnPageChangeCallback : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
if (isColorTransitionsEnabled && position < pagerAdapter.itemCount - 1) {
val currentSlide = getPagerItem(position)
val nextSlide = getPagerItem(position + 1)
performColorTransition(currentSlide, nextSlide, positionOffset)
}
}
override fun onPageSelected(position: Int) {
if (slidesNumber >= 1) {
indicatorController?.selectPosition(position)
}
updateButtonsVisibility()
pagerController.isPermissionSlide = this@AppIntroBase.isPermissionSlide
// Firing all the necessary Callbacks
this@AppIntroBase.onPageSelected(position)
if (slidesNumber > 0) {
if (currentlySelectedItem == -1) {
dispatchSlideChangedCallbacks(
null,
getPagerItem(position)
)
} else {
dispatchSlideChangedCallbacks(
getPagerItem(currentlySelectedItem),
getPagerItem(pagerController.getCurrentItem())
)
}
}
currentlySelectedItem = position
}
}
private companion object {
private val TAG = LogHelper.makeLogTag(AppIntroBase::class.java)
private const val DEFAULT_VIBRATE_DURATION = 20L
private const val PERMISSIONS_REQUEST_ALL_PERMISSIONS = 1
private const val ARG_BUNDLE_COLOR_TRANSITIONS_ENABLED = "colorTransitionEnabled"
private const val ARG_BUNDLE_CURRENT_ITEM = "currentItem"
private const val ARG_BUNDLE_IS_BUTTONS_ENABLED = "isButtonsEnabled"
private const val ARG_BUNDLE_IS_FULL_PAGING_ENABLED = "isFullPagingEnabled"
private const val ARG_BUNDLE_IS_INDICATOR_ENABLED = "isIndicatorEnabled"
private const val ARG_BUNDLE_IS_SKIP_BUTTON_ENABLED = "isSkipButtonsEnabled"
private const val ARG_BUNDLE_PERMISSION_MAP = "permissionMap"
private const val ARG_BUNDLE_RETAIN_IS_BUTTONS_ENABLED = "retainIsButtonsEnabled"
private const val ARG_BUNDLE_SLIDES_NUMBER = "slidesNumber"
}
}
/** Extension property to toggle visibility/invisibility of a view */
private var View.isVisible: Boolean
get() = this.visibility == View.VISIBLE
set(value) {
this.visibility = if (value) View.VISIBLE else View.INVISIBLE
}
@@ -0,0 +1,182 @@
package com.github.appintro
import android.graphics.drawable.Animatable
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.github.appintro.internal.LogHelper
import com.github.appintro.internal.TypefaceContainer
abstract class AppIntroBaseFragment : Fragment(), SlideSelectionListener, SlideBackgroundColorHolder {
private val viewModel: AppIntroFragmentViewModel by viewModels()
private val logTAG = LogHelper.makeLogTag(AppIntroBaseFragment::class.java)
@get:LayoutRes
protected abstract val layoutId: Int
@ColorInt
@Deprecated(
"`defaultBackgroundColor` has been deprecated to support configuration changes",
ReplaceWith("defaultBackgroundColorRes")
)
final override var defaultBackgroundColor: Int = 0
private set
@ColorRes
final override var defaultBackgroundColorRes: Int = 0
private set
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@Suppress("DEPRECATION")
defaultBackgroundColor = viewModel.defaultBackgroundColor ?: 0
defaultBackgroundColorRes = viewModel.defaultBackgroundColorRes ?: 0
val args = arguments
if (args != null && args.size() != 0) {
viewModel.drawable = args.getInt(ARG_DRAWABLE)
viewModel.title = args.getCharSequence(ARG_TITLE)
viewModel.description = args.getCharSequence(ARG_DESC)
viewModel.bgDrawable = args.getInt(ARG_BG_DRAWABLE)
viewModel.titleTypefaceUrl = args.getString(ARG_TITLE_TYPEFACE_URL)
viewModel.titleTypefaceRes = args.getInt(ARG_TITLE_TYPEFACE_RES)
viewModel.descTypefaceUrl = args.getString(ARG_DESC_TYPEFACE_URL)
viewModel.descTypefaceRes = args.getInt(ARG_DESC_TYPEFACE_RES)
@Suppress("DEPRECATION")
viewModel.defaultBackgroundColor = args.getInt(ARG_BG_COLOR)
viewModel.defaultBackgroundColorRes = args.getInt(ARG_BG_COLOR_RES)
if (args.containsKey(ARG_TITLE_COLOR)) {
viewModel.titleColor = args.getInt(ARG_TITLE_COLOR)
}
if (args.containsKey(ARG_TITLE_COLOR_RES)) {
viewModel.titleColorRes = args.getInt(ARG_TITLE_COLOR_RES)
}
if (args.containsKey(ARG_DESC_COLOR)) {
viewModel.descColor = args.getInt(ARG_DESC_COLOR)
}
if (args.containsKey(ARG_DESC_COLOR_RES)) {
viewModel.descColorRes = args.getInt(ARG_DESC_COLOR_RES)
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(layoutId, container, false)
val titleText = view.findViewById<TextView>(R.id.title)
val descriptionText = view.findViewById<TextView>(R.id.description)
val slideImage = view.findViewById<ImageView>(R.id.image)
val mainLayout = view.findViewById<ConstraintLayout>(R.id.main)
titleText.text = viewModel.title
descriptionText.text = viewModel.description
val titleColorRes = viewModel.titleColorRes
val titleColor = viewModel.titleColor
if (titleColorRes != null) {
titleText.setTextColor(ContextCompat.getColor(requireContext(), titleColorRes))
} else if (titleColor != null) { // Fallback to deprecated static color
titleText.setTextColor(titleColor)
}
val descColorRes = viewModel.descColorRes
val descColor = viewModel.descColor
if (descColorRes != null) {
descriptionText.setTextColor(ContextCompat.getColor(requireContext(), descColorRes))
} else if (descColor != null) { // Fallback to deprecated static color
descriptionText.setTextColor(descColor)
}
TypefaceContainer(
typeFaceUrl = viewModel.titleTypefaceUrl,
typeFaceResource = viewModel.titleTypefaceRes ?: 0
).applyTo(titleText)
TypefaceContainer(
typeFaceUrl = viewModel.descTypefaceUrl,
typeFaceResource = viewModel.descTypefaceRes ?: 0
).applyTo(descriptionText)
viewModel.drawable?.let {
slideImage.setImageResource(it)
}
val bgDrawable = viewModel.bgDrawable
when {
bgDrawable != null -> {
mainLayout?.setBackgroundResource(bgDrawable)
}
defaultBackgroundColorRes != 0 -> {
mainLayout?.setBackgroundColor(ContextCompat.getColor(requireContext(), defaultBackgroundColorRes))
}
else -> {
@Suppress("DEPRECATION")
mainLayout?.setBackgroundColor(defaultBackgroundColor)
}
}
titleText.movementMethod = ScrollingMovementMethod()
descriptionText.movementMethod = ScrollingMovementMethod()
return view
}
override fun onResume() {
super.onResume()
view?.findViewById<ImageView>(R.id.image).let {
if (it is Animatable) {
it.start()
}
}
}
override fun onPause() {
super.onPause()
view?.findViewById<ImageView>(R.id.image).let {
if (it is Animatable) {
it.start()
}
}
}
override fun onSlideDeselected() {
LogHelper.d(logTAG, "Slide ${viewModel.title} has been deselected.")
}
override fun onSlideSelected() {
LogHelper.d(logTAG, "Slide ${viewModel.title} has been selected.")
}
override fun setBackgroundColor(@ColorInt backgroundColor: Int) {
view?.findViewById<ConstraintLayout>(R.id.main)?.setBackgroundColor(backgroundColor)
}
}
@@ -0,0 +1,43 @@
package com.github.appintro
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
/**
* Util class to be used when creating a slide with a custom layout.
* [AppIntroCustomLayoutFragment.newInstance] passing the Layout ID of your custom layout.
*
* You can then use this Slide with the [AppIntroBase.addSlide] methods to add this slide
* to your AppIntro.
*/
class AppIntroCustomLayoutFragment : Fragment() {
private var layoutResId = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
layoutResId = arguments?.getInt(ARG_LAYOUT_RES_ID) ?: 0
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(layoutResId, container, false)
companion object {
private const val ARG_LAYOUT_RES_ID = "layoutResId"
@JvmStatic
fun newInstance(layoutResId: Int): AppIntroCustomLayoutFragment {
val customSlide = AppIntroCustomLayoutFragment()
val args = Bundle()
args.putInt(ARG_LAYOUT_RES_ID, layoutResId)
customSlide.arguments = args
return customSlide
}
}
}
@@ -0,0 +1,150 @@
package com.github.appintro
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.FontRes
import com.github.appintro.model.SliderPage
@Suppress("LongParameterList")
class AppIntroFragment : AppIntroBaseFragment() {
override val layoutId: Int get() = R.layout.appintro_fragment_intro
companion object {
/**
* Generates a new instance for [AppIntroFragment]
*
* @param title CharSequence which will be the slide title
* @param description CharSequence which will be the slide description
* @param imageDrawable @DrawableRes (Integer) the image that will be
* displayed, obtained from Resources
* @param backgroundColor @ColorInt (Integer) custom background color
* @param titleColor @ColorInt (Integer) custom title color
* @param descriptionColor @ColorInt (Integer) custom description color
* @param titleTypefaceFontRes @FontRes (Integer) custom title typeface obtained
* from Resources
* @param descriptionTypefaceFontRes @FontRes (Integer) custom description typeface obtained
* from Resources
* @param backgroundDrawable @DrawableRes (Integer) custom background drawable
*
* @return An [AppIntroFragment] created instance
*/
@JvmOverloads
@JvmStatic
@Deprecated(
"`newInstance` is deprecated to support color resources instead of color int " +
"for configuration changes and dark theme support",
ReplaceWith(
"createInstance(title, description, imageDrawable, backgroundColor, " +
"titleColor, descriptionColor, titleTypefaceFontRes, descriptionTypefaceFontRes, " +
"backgroundDrawable)"
)
)
fun newInstance(
title: CharSequence? = null,
description: CharSequence? = null,
@DrawableRes imageDrawable: Int? = null,
@ColorInt backgroundColor: Int? = null,
@ColorInt titleColor: Int? = null,
@ColorInt descriptionColor: Int? = null,
@FontRes titleTypefaceFontRes: Int? = null,
@FontRes descriptionTypefaceFontRes: Int? = null,
@DrawableRes backgroundDrawable: Int? = null
): AppIntroFragment {
return createInstance(
SliderPage(
title = title,
description = description,
imageDrawable = imageDrawable,
backgroundColor = backgroundColor,
titleColor = titleColor,
descriptionColor = descriptionColor,
titleTypefaceFontRes = titleTypefaceFontRes,
descriptionTypefaceFontRes = descriptionTypefaceFontRes,
backgroundDrawable = backgroundDrawable
)
)
}
/**
* Generates a new instance for [AppIntroFragment]
*
* @param title CharSequence which will be the slide title
* @param description CharSequence which will be the slide description
* @param imageDrawable @DrawableRes (Integer) the image that will be
* displayed, obtained from Resources
* @param backgroundColorRes @ColorRes (Integer) custom background color
* @param titleColorRes @ColorRes (Integer) custom title color
* @param descriptionColorRes @ColorRes (Integer) custom description color
* @param titleTypefaceFontRes @FontRes (Integer) custom title typeface obtained
* from Resources
* @param descriptionTypefaceFontRes @FontRes (Integer) custom description typeface obtained
* from Resources
* @param backgroundDrawable @DrawableRes (Integer) custom background drawable
*
* @return An [AppIntroFragment] created instance
*/
@JvmOverloads
@JvmStatic
fun createInstance(
title: CharSequence? = null,
description: CharSequence? = null,
@DrawableRes imageDrawable: Int? = null,
@ColorRes backgroundColorRes: Int? = null,
@ColorRes titleColorRes: Int? = null,
@ColorRes descriptionColorRes: Int? = null,
@FontRes titleTypefaceFontRes: Int? = null,
@FontRes descriptionTypefaceFontRes: Int? = null,
@DrawableRes backgroundDrawable: Int? = null
): AppIntroFragment {
return createInstance(
SliderPage(
title = title,
description = description,
imageDrawable = imageDrawable,
backgroundColorRes = backgroundColorRes,
titleColorRes = titleColorRes,
descriptionColorRes = descriptionColorRes,
titleTypefaceFontRes = titleTypefaceFontRes,
descriptionTypefaceFontRes = descriptionTypefaceFontRes,
backgroundDrawable = backgroundDrawable
)
)
}
/**
* Generates an [AppIntroFragment] from a given [SliderPage]
*
* @param sliderPage the [SliderPage] object which contains all attributes for
* the current slide
*
* @return An [AppIntroFragment] created instance
*/
@JvmStatic
@Deprecated(
"`newInstance` is deprecated to support color resources instead of color int " +
"for configuration changes and dark theme support",
ReplaceWith(
"createInstance(sliderPage)"
)
)
fun newInstance(sliderPage: SliderPage) = createInstance(sliderPage)
/**
* Generates an [AppIntroFragment] from a given [SliderPage]
*
* @param sliderPage the [SliderPage] object which contains all attributes for
* the current slide
*
* @return An [AppIntroFragment] created instance
*/
@JvmStatic
fun createInstance(sliderPage: SliderPage): AppIntroFragment {
val slide = AppIntroFragment()
slide.arguments = sliderPage.toBundle()
return slide
}
}
}
@@ -0,0 +1,56 @@
package com.github.appintro
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.FontRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.github.appintro.internal.delegate
internal const val ARG_TITLE = "title"
internal const val ARG_TITLE_TYPEFACE_URL = "title_typeface"
internal const val ARG_TITLE_TYPEFACE_RES = "title_typeface_res"
internal const val ARG_DESC = "desc"
internal const val ARG_DESC_TYPEFACE_URL = "desc_typeface"
internal const val ARG_DESC_TYPEFACE_RES = "desc_typeface_res"
internal const val ARG_DRAWABLE = "drawable"
internal const val ARG_BG_COLOR = "bg_color"
internal const val ARG_BG_COLOR_RES = "bg_color_res"
internal const val ARG_TITLE_COLOR = "title_color"
internal const val ARG_TITLE_COLOR_RES = "title_color_res"
internal const val ARG_DESC_COLOR = "desc_color"
internal const val ARG_DESC_COLOR_RES = "desc_color_res"
internal const val ARG_BG_DRAWABLE = "bg_drawable"
internal class AppIntroFragmentViewModel(state: SavedStateHandle) : ViewModel() {
internal var title by state.delegate<CharSequence?>(ARG_TITLE)
internal var description by state.delegate<CharSequence?>(ARG_DESC)
internal var drawable by state.delegate<Int?>(ARG_DRAWABLE)
internal var bgDrawable by state.delegate<Int?>(ARG_BG_DRAWABLE)
@get:ColorInt
internal var titleColor by state.delegate<Int?>(ARG_TITLE_COLOR)
@get:ColorRes
internal var titleColorRes by state.delegate<Int?>(ARG_TITLE_COLOR_RES)
@get:ColorInt
internal var descColor by state.delegate<Int?>(ARG_DESC_COLOR)
@get:ColorRes
internal var descColorRes by state.delegate<Int?>(ARG_DESC_COLOR_RES)
@get:ColorRes
internal var defaultBackgroundColorRes by state.delegate<Int?>(ARG_BG_COLOR_RES)
@get:ColorInt
internal var defaultBackgroundColor by state.delegate<Int?>(ARG_BG_COLOR)
@get:FontRes
internal var titleTypefaceRes by state.delegate<Int?>(ARG_TITLE_TYPEFACE_RES)
internal var titleTypefaceUrl by state.delegate<String?>(ARG_TITLE_TYPEFACE_URL)
@get:FontRes
internal var descTypefaceRes by state.delegate<Int?>(ARG_DESC_TYPEFACE_RES)
internal var descTypefaceUrl by state.delegate<String?>(ARG_DESC_TYPEFACE_URL)
}
@@ -0,0 +1,43 @@
package com.github.appintro
import androidx.annotation.IdRes
/**
* Sealed class to represent all the possible Page Transformers
* offered by AppIntro.
*/
sealed class AppIntroPageTransformerType {
/** Sets the animation of the intro to a flow animation */
object Flow : AppIntroPageTransformerType()
/** Sets the animation of the intro to a depth animation */
object Depth : AppIntroPageTransformerType()
/** Sets the animation of the intro to a zoom animation */
object Zoom : AppIntroPageTransformerType()
/** Sets the animation of the intro to a slide over animation */
object SlideOver : AppIntroPageTransformerType()
/** Sets the animation of the intro to a fade animation */
object Fade : AppIntroPageTransformerType()
/**
* Sets the animation of the intro to a parallax animation
* @property titleParallaxFactor Parallax factor of title
* @property imageParallaxFactor Parallax factor of image
* @property descriptionParallaxFactor Parallax factor of description
* @property titleViewId The ID to use for the title view to animate
* @property imageViewId The ID to use for the image view to animate
* @property descriptionViewId The ID to use for the description view to animate
*/
class Parallax(
val titleParallaxFactor: Double = 1.0,
val imageParallaxFactor: Double = -1.0,
val descriptionParallaxFactor: Double = 2.0,
@IdRes val titleViewId: Int = R.id.title,
@IdRes val imageViewId: Int = R.id.image,
@IdRes val descriptionViewId: Int = R.id.description
) : AppIntroPageTransformerType()
}
@@ -0,0 +1,20 @@
package com.github.appintro
/**
* Register an instance of AppIntroViewPagerListener.
* Before the user swipes to the next page, this listener will be called and
* can interrupt swiping by returning false to [onCanRequestNextPage]
*
* [onIllegallyRequestedNextPage] will be called if the user tries to swipe and was not allowed
* to do so (useful for showing a toast or something similar).
*
* [onUserRequestedPermissionsDialog] will be called when the user swipes forward on a slide
* that contains permissions.
*/
interface AppIntroViewPagerListener {
fun onCanRequestNextPage(): Boolean
fun onIllegallyRequestedNextPage()
fun onUserRequestedPermissionsDialog()
}
@@ -0,0 +1,35 @@
package com.github.appintro
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
interface SlideBackgroundColorHolder {
/**
* Returns the default background color of the slide
*
* @return The default background color of the slide
*/
@get:ColorInt
@Deprecated(
"`defaultBackgroundColor` has been deprecated to support configuration changes",
ReplaceWith("defaultBackgroundColorRes")
)
val defaultBackgroundColor: Int
/**
* Returns the default background color of the slide
*
* @return The default background color of the slide
*/
@get:ColorRes
val defaultBackgroundColorRes: Int
/**
* Sets the actual background color of the slide. This does not affect the default background color.
* This method should change the background color of the slide's root layout element (e.g. LinearLayout).
*
* @param backgroundColor New actual background color.
*/
fun setBackgroundColor(@ColorInt backgroundColor: Int)
}
@@ -0,0 +1,17 @@
package com.github.appintro
interface SlidePolicy {
/**
* Whether the user has fulfilled the slides policy and should be allowed to navigate through the intro further.
* If false is returned, [.onUserIllegallyRequestedNextPage] will be called.
*
* @return True if the user should be allowed to leave the slide, else false.
*/
val isPolicyRespected: Boolean
/**
* Called if a user tries to go to the next slide while into navigation has been locked.
*/
fun onUserIllegallyRequestedNextPage()
}
@@ -0,0 +1,14 @@
package com.github.appintro
interface SlideSelectionListener {
/**
* Called when this slide becomes selected
*/
fun onSlideSelected()
/**
* Called when this slide gets deselected.
* Please note, that this method won't be called if the user exits the intro in any way.
*/
fun onSlideDeselected()
}
@@ -0,0 +1,76 @@
package com.github.appintro.indicator
import android.content.Context
import android.view.Gravity
import android.view.Gravity.CENTER
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.github.appintro.R
/**
* An [IndicatorController] that shows a list of dots and highlight the selected dot.
* Use this when the number of page you're dealing with is not too high.
*/
class DotIndicatorController(context: Context) : IndicatorController, LinearLayout(context) {
override var selectedIndicatorColor = ContextCompat.getColor(context, R.color.appintro_default_selected_color)
set(value) {
field = value
selectPosition(currentPosition)
}
override var unselectedIndicatorColor = ContextCompat.getColor(context, R.color.appintro_default_unselected_color)
set(value) {
field = value
selectPosition(currentPosition)
}
private var currentPosition = 0
private var slideCount = 0
override fun newInstance(context: Context): View {
val newLayoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT
)
newLayoutParams.gravity = Gravity.CENTER_VERTICAL
layoutParams = newLayoutParams
orientation = HORIZONTAL
gravity = CENTER
return this
}
override fun initialize(slideCount: Int) {
this.slideCount = slideCount
for (i in 0 until slideCount) {
val dot = ImageView(this.context)
dot.setImageDrawable(
ContextCompat.getDrawable(this.context, R.drawable.ic_appintro_indicator)
)
val params = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT
)
if (slideCount == 1) {
dot.visibility = View.INVISIBLE
}
this.addView(dot, params)
}
selectPosition(0)
}
override fun selectPosition(index: Int) {
currentPosition = index
for (i in 0 until slideCount) {
val tint = if (i == index) {
selectedIndicatorColor
} else {
unselectedIndicatorColor
}
(getChildAt(i) as ImageView).let { DrawableCompat.setTint(it.drawable, tint) }
}
}
}
@@ -0,0 +1,49 @@
package com.github.appintro.indicator
import android.content.Context
import android.view.View
import androidx.annotation.ColorInt
/**
* A controller that is used to provide custom indicator implementations and to control
* their behaviour. This is used for [AppIntro.setCustomIndicator] and
* [AppIntro2.setCustomIndicator]
*/
interface IndicatorController {
@get:ColorInt
var selectedIndicatorColor: Int
@get:ColorInt
var unselectedIndicatorColor: Int
/**
* Create a new instance of the view to be inserted in the AppIntro layout.
* This method is only called once for each creation of the activity.
*
* [IndicatorController.initialize] is called after this.
*
* @param context A context to be used for the view instantiation
* @return An instance of the indicator view
*/
fun newInstance(context: Context): View
/**
* Initialize the indicator view with the requested amount of elements.
* As with [IndicatorController.newInstance], this method is only called once for each
* creation of the activity as well.
*
* [IndicatorController.newInstance] is called before this.
*
* @param slideCount The amount of slides present in the AppIntro
*/
fun initialize(slideCount: Int)
/**
* Select the position for the new page that became selected.
* This method is called every time the selected page changed.
*
* @param index The index of the page that became selected
*/
fun selectPosition(index: Int)
}
@@ -0,0 +1,67 @@
package com.github.appintro.indicator
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ProgressBar
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import com.github.appintro.internal.LayoutUtil
internal const val DEFAULT_COLOR = 1
/**
* An [IndicatorController] that shows a [ProgressBar] for express the number of
* page that have been completed.
* Use this when the number of page is higher and the [DotIndicatorController]
* would not fit in the screen.
*/
class ProgressIndicatorController @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.progressBarStyleHorizontal
) : IndicatorController, ProgressBar(context, attrs, defStyleAttr) {
override var selectedIndicatorColor = DEFAULT_COLOR
set(value) {
field = value
progressDrawable.colorFilter =
BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
value,
BlendModeCompat.SRC_ATOP
)
}
override var unselectedIndicatorColor = DEFAULT_COLOR
set(value) {
field = value
progressDrawable.colorFilter =
BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
value,
BlendModeCompat.SRC_ATOP
)
}
override fun newInstance(context: Context) = this
override fun initialize(slideCount: Int) {
this.max = slideCount
if (isRtl) {
this.scaleX = -1F
}
if (slideCount == 1) {
this.visibility = View.INVISIBLE
}
selectPosition(0)
}
override fun selectPosition(index: Int) {
this.progress = if (isRtl) {
max - index
} else {
index + 1
}
}
private val isRtl: Boolean get() = LayoutUtil.isRtl(this.context)
}
@@ -0,0 +1,283 @@
package com.github.appintro.internal
import android.gesture.GestureOverlayView
import android.gesture.GestureOverlayView.OnGestureListener
import android.view.MotionEvent
import androidx.viewpager2.widget.ViewPager2
import com.github.appintro.AppIntroBase
import com.github.appintro.AppIntroPageTransformerType
import com.github.appintro.AppIntroViewPagerListener
import com.github.appintro.internal.viewpager.PagerAdapter
import com.github.appintro.internal.viewpager.ViewPagerTransformer
import kotlin.math.max
/**
* Class that controls the [ViewPager2] of AppIntro.
* This is responsible of handling of paging, managing touch and dispatching events.
*
* @property isFullPagingEnabled Enable or disable swiping at all.
* @property isPermissionSlide If the current slide has permissions.
* @property onNextPageRequestedListener Listener for Next Page events.
*/
internal class AppIntroViewPagerController(
private val viewPager: ViewPager2,
private val viewPagerGestureOverlay: GestureOverlayView
) {
var isFullPagingEnabled = true
var isPermissionSlide = false
var onNextPageRequestedListener: AppIntroViewPagerListener? = null
private var currentTouchDownX: Float = 0.toFloat()
private var currentTouchDownY: Float = 0.toFloat()
private var illegallyRequestedNextPageLastCalled: Long = 0
private var pageChangeCallback: AppIntroBase.OnPageChangeCallback? = null
init {
addPagerTouchInterceptor()
}
fun goToNextSlide() {
with(viewPager) {
// avoid IllegalStateException when changing item while fake dragging
endFakeDrag()
setCurrentViewPagerItem(
position = if (!LayoutUtil.isRtl(context)) currentItem + 1 else currentItem - 1,
smoothScrool = true
)
}
}
fun goToPreviousSlide() {
with(viewPager) {
// avoid IllegalStateException when changing item while fake dragging
endFakeDrag()
setCurrentViewPagerItem(
position = if (!LayoutUtil.isRtl(context)) currentItem - 1 else currentItem + 1,
smoothScrool = true
)
}
}
fun isFirstSlide(size: Int): Boolean {
with(viewPager) {
return if (LayoutUtil.isRtl(context)) (currentItem - size + 1 == 0) else (currentItem == 0)
}
}
fun isLastSlide(size: Int): Boolean {
with(viewPager) {
return if (LayoutUtil.isRtl(context)) (currentItem == 0) else (currentItem - size + 1 == 0)
}
}
fun getCurrentSlideNumber(size: Int): Int {
with(viewPager) {
return if (LayoutUtil.isRtl(context)) (size - currentItem) else (currentItem + 1)
}
}
/**
* Override is required to trigger [AppIntroBase.OnPageChangeCallback.onPageSelected] for the first page.
* This is needed to correctly handle progress button display after rotation on a locked first page.
*/
fun setCurrentViewPagerItem(position: Int, smoothScrool: Boolean = false) {
with(viewPager) {
endFakeDrag()
val oldItem = currentItem
viewPager.setCurrentItem(position, smoothScrool)
// When you pass set current item to 0,
// The pageChangeListener won't be called so we call it on our own
if (oldItem == 0 && position == 0) {
pageChangeCallback?.onPageSelected(0)
}
}
}
/**
* Checks for illegal sliding attempts.
* Every time the user presses the screen, the respective coordinates are stored.
* Once the user swipes/stops pressing, the new coordinates are checked against the stored ones.
* Therefore [userIllegallyRequestNextPage] is called. If this call detects an illegal swipe,
* the respective listener [onNextPageRequestedListener] gets called.
*/
private fun canPerformTouchEvent(event: MotionEvent?): Boolean {
// If paging is disabled we should ignore any viewpager touch
// (also, not display any error message)
if (!isFullPagingEnabled || event == null) {
return false
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
currentTouchDownX = event.x
currentTouchDownY = event.y
}
else -> {
if (event.action == MotionEvent.ACTION_UP) {
viewPager.performClick()
}
val canRequestNextPage = onNextPageRequestedListener?.onCanRequestNextPage() ?: true
// If user can't request the page, we shortcircuit the ACTION_MOVE logic here.
// We need to return false if we detect that the user swipes forward,
// and also call onIllegallyRequestedNextPage if the threshold was too high
// (so the user can be informed).
if (!canRequestNextPage && isSwipeForward(currentTouchDownX, event.x)) {
if (userIllegallyRequestNextPage()) {
onNextPageRequestedListener?.onIllegallyRequestedNextPage()
}
return false
}
// If the slide contains permissions, check for forward swipe.
if (isPermissionSlide && isSwipeForward(currentTouchDownX, event.x)) {
onNextPageRequestedListener?.onUserRequestedPermissionsDialog()
}
}
}
return isFullPagingEnabled
}
private var lastTouchValue: Float = 0f
/**
* Simulate touch events on the ViewPager2 using fakeDrag, since isUserInputEnabled = false
* We need this to eventually block user touches in forward if policy is not respected
*/
private fun handleOnTouchEvent(event: MotionEvent?): Boolean {
if (!canPerformTouchEvent(event)) {
return false
}
// allow the slider to "work" left and right.
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
lastTouchValue = event.x
if (!viewPager.isFakeDragging) {
viewPager.beginFakeDrag()
}
}
MotionEvent.ACTION_MOVE -> {
val value = event.x
val delta = value - lastTouchValue
viewPager.fakeDragBy(delta)
lastTouchValue = value
return true
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
viewPager.endFakeDrag()
}
}
return true
}
/**
* Util function to check if the user swiped forward.
* The direction of forward is different in RTL mode.
*/
private fun isSwipeForward(oldX: Float, newX: Float): Boolean {
with(viewPager) {
return (if (LayoutUtil.isRtl(context)) (newX > oldX) else (oldX > newX))
}
}
/**
* Util function to throttle illegallyRequestedNext to max one request per second.
*/
private fun userIllegallyRequestNextPage(): Boolean {
if (System.currentTimeMillis() - illegallyRequestedNextPageLastCalled >=
ON_ILLEGALLY_REQUESTED_NEXT_PAGE_MAX_INTERVAL
) {
illegallyRequestedNextPageLastCalled = System.currentTimeMillis()
return true
}
return false
}
/**
* Disables ViewPager swipes and handles manually touch events.
* This is because we may want to discard some swipes in a particular direction
* if policy doesn't allow that.
*
* Touch events are then forwarded (if policy allows that) to pager
* using fakeDrag feature of ViewPager2.
*/
private fun addPagerTouchInterceptor() {
// disable ViewPager swipe
// touch events will be forwarded to gesture overlay to check policies
viewPager.isUserInputEnabled = false
viewPagerGestureOverlay.addOnGestureListener(object : OnGestureListener {
override fun onGestureStarted(overlayView: GestureOverlayView?, event: MotionEvent?) {
handleOnTouchEvent(event)
}
override fun onGesture(overlayView: GestureOverlayView?, event: MotionEvent?) {
handleOnTouchEvent(event)
}
override fun onGestureEnded(overlayView: GestureOverlayView?, event: MotionEvent?) {
handleOnTouchEvent(event)
}
override fun onGestureCancelled(overlayView: GestureOverlayView?, event: MotionEvent?) {
handleOnTouchEvent(event)
}
})
}
internal fun reCenterCurrentSlide() {
// Hacky way to force a recenter of the ViewPager to the current slide.
// We perform a page back and forward to recenter the ViewPager at the current position.
// This is needed as we're interrupting the user Swipe due to Permissions.
// If the user denies a permission, we want to recenter the slide.
with(viewPager) {
val item = currentItem
setCurrentViewPagerItem(max(item - 1, 0), false)
setCurrentViewPagerItem(item, false)
}
}
internal fun registerOnPageChangeCallback(callback: AppIntroBase.OnPageChangeCallback) {
viewPager.registerOnPageChangeCallback(callback)
this.pageChangeCallback = callback
}
fun setAppIntroPageTransformer(appIntroTransformer: AppIntroPageTransformerType) {
with(viewPager) {
setPageTransformer(ViewPagerTransformer(appIntroTransformer))
}
}
fun setPageTransformer(pageTransformer: ViewPager2.PageTransformer?) {
with(viewPager) {
setPageTransformer(pageTransformer)
}
}
fun setOffscreenPageLimit(offscreenPageLimit: Int) {
viewPager.offscreenPageLimit = offscreenPageLimit
}
fun setAdapter(pagerAdapter: PagerAdapter) {
viewPager.adapter = pagerAdapter
}
fun post(function: () -> Unit) {
viewPager.post(function)
}
fun getCurrentItem() = viewPager.currentItem
private companion object {
private const val ON_ILLEGALLY_REQUESTED_NEXT_PAGE_MAX_INTERVAL = 1000
}
}
@@ -0,0 +1,32 @@
package com.github.appintro.internal
import android.content.Context
import android.graphics.Typeface
import androidx.core.content.res.ResourcesCompat
/**
* Custom Font Cache Implementation.
* Prevent(s) memory leaks due to Typeface objects
*/
internal object CustomFontCache {
private val TAG = LogHelper.makeLogTag(CustomFontCache::class)
private val cache = hashMapOf<String, Typeface>()
fun getFont(ctx: Context, path: String?, fontCallback: ResourcesCompat.FontCallback) {
if (path.isNullOrEmpty()) {
LogHelper.w(TAG, "Empty typeface path provided!")
return
}
cache[path]?.let {
// Cache hit! Return the typeface.
fontCallback.onFontRetrieved(it)
} ?: run {
// Cache miss! Create the typeface and store it.
val newTypeface = Typeface.createFromAsset(ctx.assets, path)
cache[path] = newTypeface
fontCallback.onFontRetrieved(newTypeface)
}
}
}
@@ -0,0 +1,17 @@
package com.github.appintro.internal
import android.content.Context
import android.os.Build
import android.view.View
/**
* Util object for interacting with Layouts
*/
internal object LayoutUtil {
@JvmStatic
fun isRtl(ctx: Context): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
ctx.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
}
}
@@ -0,0 +1,66 @@
package com.github.appintro.internal
import android.util.Log
import kotlin.reflect.KClass
private const val LOG_PREFIX = "Log: "
private const val LOG_PREFIX_LENGTH = LOG_PREFIX.length
private const val MAX_LOG_TAG_LENGTH = 23
/**
* Helper object to interact with the Android [Log] class.
*/
internal object LogHelper {
/**
* Creates a tag for the logs from a [Class]
* Don't use this when obfuscating class names!
*/
@JvmStatic
fun makeLogTag(cls: Class<*>) =
LOG_PREFIX +
cutTagLength(cls.simpleName, MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH)
/**
* Creates a tag for the logs from a [KClass]
* Don't use this when obfuscating class names!
*/
fun makeLogTag(cls: KClass<*>) =
LOG_PREFIX +
cutTagLength(cls.simpleName ?: "", MAX_LOG_TAG_LENGTH - LOG_PREFIX_LENGTH)
private fun cutTagLength(tag: String, length: Int): String {
return if (tag.length > length) {
tag.substring(0, length - 1)
} else {
tag
}
}
@JvmStatic
fun d(tag: String, message: String) = Log.d(tag, message)
@JvmStatic
fun v(tag: String, message: String) = Log.v(tag, message)
@JvmStatic
fun i(tag: String, message: String) = Log.i(tag, message)
@JvmOverloads
@JvmStatic
fun w(tag: String, message: String, throwable: Throwable? = null) {
Log.w(tag, message, throwable)
}
@JvmOverloads
@JvmStatic
fun e(tag: String, message: String, throwable: Throwable? = null) {
Log.e(tag, message, throwable)
}
@JvmOverloads
@JvmStatic
fun wtf(tag: String, message: String, throwable: Throwable? = null) {
Log.wtf(tag, message, throwable)
}
}
@@ -0,0 +1,40 @@
package com.github.appintro.internal
import java.io.Serializable
/**
* A data class that represents a set of permissions that should be requested to the user.
* @property permissions An Array of Permissions from the Android Framework
* @property position The position in the AppIntro pager when to request those permissions.
* @property required Whether the permission being requested needs to be granted to move forward.
*/
internal data class PermissionWrapper(
var permissions: Array<String>,
var position: Int,
var required: Boolean = true
) : Serializable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PermissionWrapper
if (!permissions.contentEquals(other.permissions)) return false
if (position != other.position) return false
if (required != other.required) return false
return true
}
override fun hashCode(): Int {
var result = permissions.contentHashCode()
result = 31 * result + position
result = 31 * result + required.hashCode()
return result
}
companion object {
private const val serialVersionUID: Long = 1L
}
}
@@ -0,0 +1,18 @@
package com.github.appintro.internal
import androidx.lifecycle.SavedStateHandle
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
inline fun <reified T> SavedStateHandle.delegate(key: String? = null): ReadWriteProperty<Any, T?> =
object : ReadWriteProperty<Any, T?> {
override fun getValue(thisRef: Any, property: KProperty<*>): T? {
val stateKey = key ?: property.name
return this@delegate[stateKey]
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {
val stateKey = key ?: property.name
this@delegate[stateKey] = value
}
}

Some files were not shown because too many files have changed in this diff Show More