Migrate from kotlin-android-extensions (synthetics) to data binding

This commit is contained in:
William Brawner 2020-12-08 16:36:42 -07:00
parent 3164bee01b
commit 53cd8c7312
18 changed files with 213 additions and 148 deletions

View file

@ -1,6 +1,22 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">

6
.idea/compiler.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" />
</component>
</project>

View file

@ -15,6 +15,7 @@
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

25
.idea/jarRepositories.xml Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinScriptingSettings">
<scriptDefinition className="org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate" definitionName="KotlinBuildScript">
<order>2147483647</order>
<autoReloadConfigurations>true</autoReloadConfigurations>
</scriptDefinition>
</component>
</project>

View file

@ -5,7 +5,7 @@
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" /> <configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations> </configurations>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View file

@ -1,6 +1,5 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
try { try {
@ -47,6 +46,9 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "1.8"
} }
buildFeatures {
viewBinding true
}
} }
dependencies { dependencies {
@ -54,17 +56,17 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "org.koin:koin-androidx-viewmodel:$koin_version" implementation "org.koin:koin-androidx-viewmodel:$koin_version"
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.2.0-alpha06' implementation 'com.google.android.material:material:1.3.0-alpha04'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.security:security-crypto:1.0.0-rc01' implementation 'androidx.security:security-crypto:1.0.0-rc01'
implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
def navigation_version = '2.2.2' def navigation_version = '2.3.2'
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
def lifecycle_version = '2.2.0' def lifecycle_version = '2.2.0'

View file

@ -10,45 +10,47 @@ import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation import android.view.animation.RotateAnimation
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_add_pi_hole.* import com.wbrawner.pihelper.databinding.FragmentAddPiHoleBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext
class AddPiHoleFragment : Fragment(), CoroutineScope { class AddPiHoleFragment : Fragment() {
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: AddPiHelperViewModel by inject() private val viewModel: AddPiHelperViewModel by inject()
private var _binding: FragmentAddPiHoleBinding? = null
private val binding get() = _binding!!
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_add_pi_hole, container, false) ): View {
_binding = FragmentAddPiHoleBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController() val navController = findNavController()
scanNetworkButton.setOnClickListener { binding.scanNetworkButton.setOnClickListener {
navController.navigate( navController.navigate(
R.id.action_addPiHoleFragment_to_scanNetworkFragment, R.id.action_addPiHoleFragment_to_scanNetworkFragment,
null, null,
null, null,
FragmentNavigatorExtras(piHelperLogo to "piHelperLogo") FragmentNavigatorExtras(binding.piHelperLogo to "piHelperLogo")
) )
} }
ipAddress.setOnEditorActionListener { _, _, _ -> binding.ipAddress.setOnEditorActionListener { _, _, _ ->
connectButton.performClick() binding.connectButton.performClick()
} }
connectButton.setSuspendingOnClickListener(this) { binding.connectButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true) showProgress(true)
if (viewModel.connectToIpAddress(ipAddress.text.toString())) { if (viewModel.connectToIpAddress(binding.ipAddress.text.toString())) {
navController.navigate( navController.navigate(
R.id.action_addPiHoleFragment_to_retrieveApiKeyFragment, R.id.action_addPiHoleFragment_to_retrieveApiKeyFragment,
null, null,
null, null,
FragmentNavigatorExtras(piHelperLogo to "piHelperLogo") FragmentNavigatorExtras(binding.piHelperLogo to "piHelperLogo")
) )
} else { } else {
AlertDialog.Builder(view.context) AlertDialog.Builder(view.context)
@ -63,7 +65,7 @@ class AddPiHoleFragment : Fragment(), CoroutineScope {
private fun showProgress(show: Boolean) { private fun showProgress(show: Boolean) {
if (show) { if (show) {
piHelperLogo.startAnimation( binding.piHelperLogo.startAnimation(
RotateAnimation( RotateAnimation(
0f, 0f,
360f, 360f,
@ -81,13 +83,13 @@ class AddPiHoleFragment : Fragment(), CoroutineScope {
} }
) )
} else { } else {
piHelperLogo.clearAnimation() binding.piHelperLogo.clearAnimation()
} }
connectionForm.visibility = if (show) View.GONE else View.VISIBLE binding.connectionForm.visibility = if (show) View.GONE else View.VISIBLE
} }
override fun onDestroyView() { override fun onDestroyView() {
cancel()
super.onDestroyView() super.onDestroyView()
_binding = null
} }
} }

View file

@ -15,16 +15,13 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.wbrawner.pihelper.MainActivity.Companion.ACTION_FORGET_PIHOLE import com.wbrawner.pihelper.MainActivity.Companion.ACTION_FORGET_PIHOLE
import kotlinx.android.synthetic.main.fragment_info.* import com.wbrawner.pihelper.databinding.FragmentInfoBinding
import kotlinx.android.synthetic.main.fragment_main.toolbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext
class InfoFragment : Fragment(), CoroutineScope { class InfoFragment : Fragment() {
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: AddPiHelperViewModel by inject() private val viewModel: AddPiHelperViewModel by inject()
private var _binding: FragmentInfoBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -34,20 +31,23 @@ class InfoFragment : Fragment(), CoroutineScope {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_info, container, false) ): View {
_binding = FragmentInfoBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar) (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
(activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) (activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true)
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.action_settings) (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.action_settings)
val html = getString(R.string.content_info) val html = getString(R.string.content_info)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
infoContent?.text = if (Build.VERSION.SDK_INT < 24) binding.infoContent.text = if (Build.VERSION.SDK_INT < 24)
Html.fromHtml(html) Html.fromHtml(html)
else else
Html.fromHtml(html, 0) Html.fromHtml(html, 0)
infoContent.movementMethod = LinkMovementMethod.getInstance() binding.infoContent.movementMethod = LinkMovementMethod.getInstance()
forgetPiHoleButton?.setOnClickListener { binding.forgetPiHoleButton.setOnClickListener {
AlertDialog.Builder(view.context) AlertDialog.Builder(view.context)
.setTitle(R.string.confirm_forget_pihole) .setTitle(R.string.confirm_forget_pihole)
.setMessage(R.string.warning_cannot_be_undone) .setMessage(R.string.warning_cannot_be_undone)
@ -77,4 +77,9 @@ class InfoFragment : Fragment(), CoroutineScope {
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
} }

View file

@ -9,6 +9,7 @@ import androidx.navigation.findNavController
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE
import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE
import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION
import com.wbrawner.pihelper.databinding.ActivityMainBinding
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -19,10 +20,9 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
window.setBackgroundDrawable(ColorDrawable(getColor(R.color.colorSurface))) window.setBackgroundDrawable(ColorDrawable(getColor(R.color.colorSurface)))
val analyticsBundle = Bundle()
analyticsBundle.putString("intent_action", intent.action)
val args = when (intent.action) { val args = when (intent.action) {
ACTION_ENABLE -> { ACTION_ENABLE -> {
if (addPiHoleViewModel.apiKey == null) { if (addPiHoleViewModel.apiKey == null) {

View file

@ -11,32 +11,28 @@ import android.view.*
import android.view.animation.Animation import android.view.animation.Animation
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation import android.view.animation.RotateAnimation
import android.widget.EditText
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.getColor import androidx.core.content.ContextCompat.getColor
import androidx.core.text.set import androidx.core.text.set
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.wbrawner.pihelper.databinding.DialogDisableCustomTimeBinding
import com.wbrawner.pihelper.databinding.FragmentMainBinding
import com.wbrawner.piholeclient.Status import com.wbrawner.piholeclient.Status
import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext
class MainFragment : Fragment(), CoroutineScope { class MainFragment : Fragment() {
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: PiHelperViewModel by inject() private val viewModel: PiHelperViewModel by inject()
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
launch { lifecycleScope.launch {
if (arguments?.getBoolean(ACTION_ENABLE) == true) { if (arguments?.getBoolean(ACTION_ENABLE) == true) {
viewModel.enablePiHole() viewModel.enablePiHole()
} else if (arguments?.getBoolean(ACTION_DISABLE) == true) { } else if (arguments?.getBoolean(ACTION_DISABLE) == true) {
@ -44,52 +40,53 @@ class MainFragment : Fragment(), CoroutineScope {
} }
viewModel.monitorSummary() viewModel.monitorSummary()
} }
viewModel.status.observe(this, Observer { viewModel.status.observe(this, {
showProgress(false) showProgress(false)
val (statusColor, statusText) = when (it) { val (statusColor, statusText) = when (it) {
Status.DISABLED -> { Status.DISABLED -> {
enableButton?.visibility = View.VISIBLE binding.enableButton.visibility = View.VISIBLE
disableButtons?.visibility = View.GONE binding.disableButtons.visibility = View.GONE
Pair(R.color.colorDisabled, R.string.status_disabled) Pair(R.color.colorDisabled, R.string.status_disabled)
} }
Status.ENABLED -> { Status.ENABLED -> {
enableButton?.visibility = View.GONE binding.enableButton.visibility = View.GONE
disableButtons?.visibility = View.VISIBLE binding.disableButtons.visibility = View.VISIBLE
Pair(R.color.colorEnabled, R.string.status_enabled) Pair(R.color.colorEnabled, R.string.status_enabled)
} }
else -> { else -> {
enableButton?.visibility = View.GONE binding.enableButton.visibility = View.GONE
disableButtons?.visibility = View.GONE binding.disableButtons.visibility = View.GONE
Pair(R.color.colorUnknown, R.string.status_unknown) Pair(R.color.colorUnknown, R.string.status_unknown)
} }
} }
status?.let { textView -> val status = getString(statusText)
val status = getString(statusText) val statusLabel = getString(R.string.label_status, status)
val statusLabel = getString(R.string.label_status, status) val start = statusLabel.indexOf(status)
val start = statusLabel.indexOf(status) val end = start + status.length
val end = start + status.length val statusSpan = SpannableString(statusLabel)
val statusSpan = SpannableString(statusLabel) statusSpan[start, end] = StyleSpan(Typeface.BOLD)
statusSpan[start, end] = StyleSpan(Typeface.BOLD) statusSpan[start, end] =
statusSpan[start, end] = ForegroundColorSpan(getColor(binding.status.context, statusColor))
ForegroundColorSpan(getColor(textView.context, statusColor)) binding.status.text = statusSpan
textView.text = statusSpan
}
}) })
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_main, container, false) ): View {
_binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main, menu) inflater.inflate(R.menu.main, menu)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar) (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
showProgress(true) showProgress(true)
enableButton?.setSuspendingOnClickListener(this) { binding.enableButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true) showProgress(true)
try { try {
viewModel.enablePiHole() viewModel.enablePiHole()
@ -97,7 +94,7 @@ class MainFragment : Fragment(), CoroutineScope {
Log.e("Pi-helper", "Failed to enable Pi-Hole", ignored) Log.e("Pi-helper", "Failed to enable Pi-Hole", ignored)
} }
} }
disable10SecondsButton?.setSuspendingOnClickListener(this) { binding.disable10SecondsButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true) showProgress(true)
try { try {
viewModel.disablePiHole(10) viewModel.disablePiHole(10)
@ -105,7 +102,7 @@ class MainFragment : Fragment(), CoroutineScope {
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored) Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
} }
} }
disable30SecondsButton?.setSuspendingOnClickListener(this) { binding.disable30SecondsButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true) showProgress(true)
try { try {
viewModel.disablePiHole(30) viewModel.disablePiHole(30)
@ -113,7 +110,7 @@ class MainFragment : Fragment(), CoroutineScope {
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored) Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
} }
} }
disable5MinutesButton?.setSuspendingOnClickListener(this) { binding.disable5MinutesButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true) showProgress(true)
try { try {
viewModel.disablePiHole(300) viewModel.disablePiHole(300)
@ -121,40 +118,43 @@ class MainFragment : Fragment(), CoroutineScope {
Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored) Log.e("Pi-helper", "Failed to disable Pi-Hole", ignored)
} }
} }
disableCustomTimeButton?.setOnClickListener { binding.disableCustomTimeButton.setOnClickListener {
val dialogView = LayoutInflater.from(it.context) val dialogView = DialogDisableCustomTimeBinding.inflate(
.inflate(R.layout.dialog_disable_custom_time, view as ViewGroup, false) LayoutInflater.from(it.context),
view as ViewGroup,
false
)
AlertDialog.Builder(it.context) AlertDialog.Builder(it.context)
.setTitle(R.string.action_disable_custom) .setTitle(R.string.action_disable_custom)
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.action_disable, null) .setPositiveButton(R.string.action_disable, null)
.setView(dialogView) .setView(dialogView.root)
.create() .create()
.apply { .apply {
setOnShowListener { setOnShowListener {
getButton(AlertDialog.BUTTON_POSITIVE).setSuspendingOnClickListener(this@MainFragment) { getButton(AlertDialog.BUTTON_POSITIVE).setSuspendingOnClickListener(
lifecycleScope
) {
try { try {
val rawTime = dialogView.findViewById<EditText>(R.id.time) val rawTime = dialogView.time
.text .text
.toString() .toString()
.toLong() .toLong()
val checkedId = val checkedId =
dialogView.findViewById<RadioGroup>(R.id.timeUnit) dialogView.timeUnit.checkedRadioButtonId
.checkedRadioButtonId
val computedTime = if (checkedId == R.id.seconds) rawTime val computedTime = if (checkedId == R.id.seconds) rawTime
else rawTime * 60 else rawTime * 60
viewModel.disablePiHole(computedTime) viewModel.disablePiHole(computedTime)
dismiss() dismiss()
} catch (e: Exception) { } catch (e: Exception) {
dialogView.findViewById<EditText>(R.id.time) dialogView.time.error = "Failed to disable Pi-hole"
.error = "Failed to disable Pi-hole"
} }
} }
} }
} }
.show() .show()
} }
disablePermanentlyButton?.setSuspendingOnClickListener(this) { binding.disablePermanentlyButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true) showProgress(true)
try { try {
viewModel.disablePiHole() viewModel.disablePiHole()
@ -173,8 +173,8 @@ class MainFragment : Fragment(), CoroutineScope {
} }
private fun showProgress(show: Boolean) { private fun showProgress(show: Boolean) {
progressBar?.visibility = if (show) { binding.progressBar.visibility = if (show) {
progressBar?.startAnimation(RotateAnimation( binding.progressBar.startAnimation(RotateAnimation(
0f, 0f,
360f, 360f,
Animation.RELATIVE_TO_SELF, Animation.RELATIVE_TO_SELF,
@ -190,10 +190,10 @@ class MainFragment : Fragment(), CoroutineScope {
}) })
View.VISIBLE View.VISIBLE
} else { } else {
progressBar?.clearAnimation() binding.progressBar.clearAnimation()
View.GONE View.GONE
} }
statusContent?.visibility = if (show) { binding.statusContent.visibility = if (show) {
View.GONE View.GONE
} else { } else {
View.VISIBLE View.VISIBLE
@ -201,7 +201,7 @@ class MainFragment : Fragment(), CoroutineScope {
} }
override fun onDestroyView() { override fun onDestroyView() {
coroutineContext[Job]?.cancel() _binding = null
super.onDestroyView() super.onDestroyView()
} }

View file

@ -11,17 +11,16 @@ import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation import android.view.animation.RotateAnimation
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import kotlinx.android.synthetic.main.fragment_retrieve_api_key.* import com.wbrawner.pihelper.databinding.FragmentRetrieveApiKeyBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext
class RetrieveApiKeyFragment : Fragment(), CoroutineScope { class RetrieveApiKeyFragment : Fragment() {
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: AddPiHelperViewModel by inject() private val viewModel: AddPiHelperViewModel by inject()
private var _binding: FragmentRetrieveApiKeyBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -36,32 +35,35 @@ class RetrieveApiKeyFragment : Fragment(), CoroutineScope {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_retrieve_api_key, container, false) ): View {
_binding = FragmentRetrieveApiKeyBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
password.setOnEditorActionListener { _, _, _ -> binding.password.setOnEditorActionListener { _, _, _ ->
connectWithPasswordButton.performClick() binding.connectWithPasswordButton.performClick()
} }
connectWithPasswordButton.setSuspendingOnClickListener(this) { binding.connectWithPasswordButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true) showProgress(true)
try { try {
viewModel.authenticateWithPassword(password.text.toString()) viewModel.authenticateWithPassword(binding.password.text.toString())
} catch (ignored: Exception) { } catch (ignored: Exception) {
Log.e("Pi-helper", "Failed to authenticate with password", ignored) Log.e("Pi-helper", "Failed to authenticate with password", ignored)
password.error = "Failed to authenticate with given password. Please verify " + binding.password.error = "Failed to authenticate with given password. Please verify " +
"you've entered it correctly and try again." "you've entered it correctly and try again."
showProgress(false) showProgress(false)
} }
} }
apiKey.setOnEditorActionListener { _, _, _ -> binding.apiKey.setOnEditorActionListener { _, _, _ ->
connectWithApiKeyButton.performClick() binding.connectWithApiKeyButton.performClick()
} }
connectWithApiKeyButton.setSuspendingOnClickListener(this) { binding.connectWithApiKeyButton.setSuspendingOnClickListener(lifecycleScope) {
showProgress(true) showProgress(true)
try { try {
viewModel.authenticateWithApiKey(apiKey.text.toString()) viewModel.authenticateWithApiKey(binding.apiKey.text.toString())
} catch (ignored: Exception) { } catch (ignored: Exception) {
apiKey.error = "Failed to authenticate with given API key. Please verify " + binding.apiKey.error = "Failed to authenticate with given API key. Please verify " +
"you've entered it correctly and try again." "you've entered it correctly and try again."
showProgress(false) showProgress(false)
} }
@ -70,7 +72,7 @@ class RetrieveApiKeyFragment : Fragment(), CoroutineScope {
private fun showProgress(show: Boolean) { private fun showProgress(show: Boolean) {
if (show) { if (show) {
piHelperLogo.startAnimation( binding.piHelperLogo.startAnimation(
RotateAnimation( RotateAnimation(
0f, 0f,
360f, 360f,
@ -88,13 +90,13 @@ class RetrieveApiKeyFragment : Fragment(), CoroutineScope {
} }
) )
} else { } else {
piHelperLogo.clearAnimation() binding.piHelperLogo.clearAnimation()
} }
authenticationForm.visibility = if (show) View.GONE else View.VISIBLE binding.authenticationForm.visibility = if (show) View.GONE else View.VISIBLE
} }
override fun onDestroyView() { override fun onDestroyView() {
cancel() _binding = null
super.onDestroyView() super.onDestroyView()
} }
} }

View file

@ -16,23 +16,23 @@ import android.view.animation.RotateAnimation
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.transition.Transition import androidx.transition.Transition
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import kotlinx.android.synthetic.main.fragment_scan_network.* import com.wbrawner.pihelper.databinding.FragmentScanNetworkBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import java.net.Inet4Address import java.net.Inet4Address
import kotlin.coroutines.CoroutineContext
class ScanNetworkFragment : Fragment(), CoroutineScope { class ScanNetworkFragment : Fragment() {
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: AddPiHelperViewModel by inject() private val viewModel: AddPiHelperViewModel by inject()
private var _binding: FragmentScanNetworkBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -60,12 +60,15 @@ class ScanNetworkFragment : Fragment(), CoroutineScope {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_scan_network, container, false) ): View {
_binding = FragmentScanNetworkBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.scanningIp.observe(viewLifecycleOwner, Observer { viewModel.scanningIp.observe(viewLifecycleOwner, Observer {
ipAddress?.text = it binding.ipAddress.text = it
}) })
viewModel.piHoleIpAddress.observe(viewLifecycleOwner, Observer { ipAddress -> viewModel.piHoleIpAddress.observe(viewLifecycleOwner, Observer { ipAddress ->
if (ipAddress == null) { if (ipAddress == null) {
@ -78,7 +81,7 @@ class ScanNetworkFragment : Fragment(), CoroutineScope {
.show() .show()
return@Observer return@Observer
} }
piHelperLogo?.animation?.let { binding.piHelperLogo.animation?.let {
it.setAnimationListener(object : Animation.AnimationListener { it.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) { override fun onAnimationRepeat(animation: Animation?) {
} }
@ -93,7 +96,7 @@ class ScanNetworkFragment : Fragment(), CoroutineScope {
it.repeatCount = 0 it.repeatCount = 0
} ?: navigateToApiKeyScreen() } ?: navigateToApiKeyScreen()
}) })
launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
if (BuildConfig.DEBUG && Build.MODEL == "Android SDK built for x86") { if (BuildConfig.DEBUG && Build.MODEL == "Android SDK built for x86") {
// For emulators, just begin scanning the host machine directly // For emulators, just begin scanning the host machine directly
viewModel.beginScanning("10.0.2.2") viewModel.beginScanning("10.0.2.2")
@ -120,9 +123,9 @@ class ScanNetworkFragment : Fragment(), CoroutineScope {
} }
} }
} }
launch { lifecycleScope.launch {
delay(500) delay(500)
if (piHelperLogo?.animation == null) { if (binding.piHelperLogo.animation == null) {
animatePiHelperLogo() animatePiHelperLogo()
} }
} }
@ -130,7 +133,7 @@ class ScanNetworkFragment : Fragment(), CoroutineScope {
private fun navigateToApiKeyScreen() { private fun navigateToApiKeyScreen() {
val extras = FragmentNavigatorExtras( val extras = FragmentNavigatorExtras(
piHelperLogo to "piHelperLogo" binding.piHelperLogo to "piHelperLogo"
) )
findNavController().navigate( findNavController().navigate(
@ -142,13 +145,13 @@ class ScanNetworkFragment : Fragment(), CoroutineScope {
} }
override fun onDestroyView() { override fun onDestroyView() {
piHelperLogo.clearAnimation() binding.piHelperLogo.clearAnimation()
cancel() _binding = null
super.onDestroyView() super.onDestroyView()
} }
private fun animatePiHelperLogo() { private fun animatePiHelperLogo() {
piHelperLogo?.startAnimation( binding.piHelperLogo.startAnimation(
RotateAnimation( RotateAnimation(
0f, 0f,
360f, 360f,

View file

@ -1,17 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:background="@color/colorSurface"> android:fitsSystemWindows="true"
app:defaultNavHost="true"
<fragment app:navGraph="@navigation/nav_graph"
android:id="@+id/content_main" tools:background="@color/colorSurface" />
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</merge>

View file

@ -23,6 +23,6 @@
</style> </style>
<style name="AppTheme.Button.Red" parent="AppTheme.Button"> <style name="AppTheme.Button.Red" parent="AppTheme.Button">
<item name="backgroundTint">@color/colorRedDark</item> <item name="backgroundTint">@color/colorAccent</item>
</style> </style>
</resources> </resources>

View file

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.61' ext.kotlin_version = '1.4.20'
ext.coroutines_version = '1.3.2' ext.coroutines_version = '1.3.2'
ext.koin_version = '2.0.1' ext.koin_version = '2.0.1'
ext.okhttp_version = '4.2.2' ext.okhttp_version = '4.2.2'
@ -8,7 +8,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.2' classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View file

@ -1,6 +1,5 @@
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
android { android {