mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
Co-authored-by: Akash Yadav <itsaky01@gmail.com>
This commit is contained in:
committed by
GitHub
parent
70ba8a54c7
commit
d942d81b5c
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
@@ -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
|
||||
}
|
||||
}
|
||||
+80
@@ -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)
|
||||
+296
@@ -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) {}
|
||||
}
|
||||
+71
@@ -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)))
|
||||
}
|
||||
}
|
||||
+83
@@ -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
|
||||
}
|
||||
+13
-11
@@ -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
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">Сборка в процессе…</string>
|
||||
<string name="build_status_failed">Сборка завершилась неудачей. Дополнительные сведения см. в выводе сборки.</string>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -74,6 +74,7 @@ include(
|
||||
":lsp:java",
|
||||
":lsp:xml",
|
||||
":subprojects:aaptcompiler",
|
||||
":subprojects:appintro",
|
||||
":subprojects:builder-model-impl",
|
||||
":subprojects:flashbar",
|
||||
":subprojects:framework-stubs",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+43
@@ -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()
|
||||
}
|
||||
+76
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+49
@@ -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)
|
||||
}
|
||||
+67
@@ -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)
|
||||
}
|
||||
+283
@@ -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
Reference in New Issue
Block a user