user16298059
user16298059

Reputation:

Alert Dialog Simplification

I'm trying to write a simplification of the AlertDialog, so I can be more at home with kotlin, if I cannot make my own language do Android then make kotlin act like PureBasic would is the theory.

The problem I've ran into is this error:

4027-4027/com.example.cardgamexxx E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.cardgamexxx, PID: 4027
android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:1444)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:469)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:114)
    at android.app.Dialog.show(Dialog.java:505)
    at android.app.AlertDialog$Builder.show(AlertDialog.java:1157)
    at com.example.cardgamexxx.Requester.messageRequester(Requester.kt:33)
    at com.example.cardgamexxx.MainActivity.onCreate$lambda-3(MainActivity.kt:695)
    at com.example.cardgamexxx.MainActivity.lambda$Zt8ODWx7LLRfq4535q7hQg5xQfw(Unknown Source:0)
    at com.example.cardgamexxx.-$$Lambda$MainActivity$Zt8ODWx7LLRfq4535q7hQg5xQfw.onClick(Unknown Source:2)
    at android.view.View.performClick(View.java:8160)
    at android.widget.TextView.performClick(TextView.java:16222)
    at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1119)
    at android.view.View.performClickInternal(View.java:8137)
    at android.view.View.access$3700(View.java:888)
    at android.view.View$PerformClick.run(View.java:30236)
    at android.os.Handler.handleCallback(Handler.java:938)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:246)
    at android.app.ActivityThread.main(ActivityThread.java:8512)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)

With this code:

package com.example.cardgamexxx

import android.app.AlertDialog
import android.content.Context
import com.example.cardgamexxx.RequesterType.MessageRequester_Ok
import com.example.cardgamexxx.RequesterType.MessageRequester_YesNo
import com.example.cardgamexxx.RequesterType.MessageRequester_YesNoCancel

//------------------------------------------------------------------------------
//  Very PureBasic'ish 'Requester' dialogs.
//------------------------------------------------------------------------------

object RequesterType {
    const val MessageRequester_Ok = 1            // to have the 'ok' only button (default)
    const val MessageRequester_YesNo = 2         // to have 'yes' or 'no' buttons
    const val MessageRequester_YesNoCancel = 3   // to have 'yes', 'no' and 'cancel' buttons
}

class Requester {

    fun inputRequester() {

    }

    fun messageRequester(appContext: Context,title: String,message: String,flags: Int) : Int {

        var result: Int = 0

        if( flags == MessageRequester_Ok ) {
            AlertDialog.Builder(appContext)
                .setTitle(title) // R.string.question_title
                .setMessage(message) // R.string.question_message
                .setPositiveButton("Ok") { _, _ -> result = 0 }
                .show()
        }

        if( flags == MessageRequester_YesNo ) {
            AlertDialog.Builder(appContext)
                .setTitle(title) // R.string.question_title
                .setMessage(message) // R.string.question_message
                .setPositiveButton("Yes") { _, _ -> result = 0 }
                .setNegativeButton("No") { _, _ -> result = 1 }
                .show()
        }

        if( flags == MessageRequester_YesNoCancel ) {
            AlertDialog.Builder(appContext)
                .setTitle(title) // R.string.question_title
                .setMessage(message) // R.string.question_message
                .setPositiveButton("Yes") { _, _ -> result = 0 }
                .setNegativeButton("No") { _, _ -> result = 1 }
                .setNegativeButton("Cancel") { _, _ -> result = 2 }
                .show()
        }

        return result

    }

    fun openFileRequester() {

    }

    fun pathRequester() {

    }

    fun saveFileRequester() {

    }

}

Another problem is the requesters while open are supposed to be 'code' blocking meaning, not that this matters all 'that' much, it would however be nice if "messageRequester()" returned a result which the code calling "messageRequester()" could then change flow according to the answer given.

Am I on the wrong path? do I need to just dump this idea and re-create the wheel and make my own from the ground up.

Upvotes: 2

Views: 1111

Answers (2)

user16298059
user16298059

Reputation:

Though I'd be nice and post the code, just in case someone else finds it useful.

Update add Input Requester

Call like this;

myreq.messageRequester(this,
                  "Testing Ok!","with this message",
                       MessageRequester_Ok) { a ->

    Log.d("debug","$a")
}

Added the last functions, openFileRequester / PathRequester

Add this class file.

package com.example.cardgamexxx

import android.app.Activity
import android.app.AlertDialog
import android.net.Uri
import android.text.InputType
import android.widget.EditText
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import com.example.cardgamexxx.RequesterType.InputRequester_Password
import com.example.cardgamexxx.RequesterType.MessageRequester_Ok
import com.example.cardgamexxx.RequesterType.MessageRequester_YesNo
import com.example.cardgamexxx.RequesterType.MessageRequester_YesNoCancel

//------------------------------------------------------------------------------
//  Very PureBasic'ish 'Requester' dialogs.
//
//  Alert Simplification
//  https://stackoverflow.com/questions/68554654/alert-dialog-simplification
//
//  Help From:
//
//  Shlomi Katriel
//  Ticherhaz FreePalestine
//------------------------------------------------------------------------------

object RequesterType {
    const val MessageRequester_Ok = 1            // to have the 'ok' only button (default)
    const val MessageRequester_YesNo = 2         // to have 'yes' or 'no' buttons
    const val MessageRequester_YesNoCancel = 3   // to have 'yes', 'no' and 'cancel' buttons
    const val InputRequester_Password = 4
}

enum class RequesterAnswer { YES, NO, CANCEL, OK }

class Requester {

    fun inputRequester(activity: Activity, title: String, message: String, flags: Int,callback: (answer: String) -> Unit) {

        val editText = EditText(activity)

        when (flags) {
            InputRequester_Password -> {
                editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
            }
        }

        val inputDialog = AlertDialog.Builder(activity)
            inputDialog.setTitle(title)
            .setView(editText)
            .setCancelable(false)
            .setNegativeButton("Cancel") { _, _ -> callback("CANCEL") }
            .setPositiveButton("OK") { _,_ -> callback (editText.text.toString()) }
            inputDialog.show()

    }

    fun messageRequester(activity: Activity, title: String, message: String, flags: Int,callback: (answer: RequesterAnswer) -> Unit) {

        if( flags == MessageRequester_Ok ) {
            AlertDialog.Builder(activity)
                .setTitle(title)
                .setMessage(message)
                .setPositiveButton("Ok") { _, _ -> callback(RequesterAnswer.OK) }
                .show()
        }

        if( flags == MessageRequester_YesNo ) {
            AlertDialog.Builder(activity)
                .setTitle(title)
                .setMessage(message)
                .setPositiveButton("Yes") { _, _ -> callback(RequesterAnswer.YES) }
                .setNegativeButton("No") { _, _ -> callback(RequesterAnswer.NO) }
                .show()
        }

        if( flags == MessageRequester_YesNoCancel ) {
            AlertDialog.Builder(activity)
                .setTitle(title)
                .setMessage(message)
                .setPositiveButton("Yes") { _, _ -> callback(RequesterAnswer.YES) }
                .setNegativeButton("No") { _, _ -> callback(RequesterAnswer.NO) }
                .setNegativeButton("Cancel") { _, _ -> callback(RequesterAnswer.CANCEL) } // doesn't work :(
                .show()
        }

    }

    fun openFileRequester(cActivity: AppCompatActivity,filter : String,callback: (answer: Uri) -> Unit) {
        val openFileRequester = cActivity.registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri ->
            callback(uri)
        }
        openFileRequester.launch(filter)
    }

    fun pathRequester(cActivity: AppCompatActivity,filter : String,callback: (answer: Uri) -> Unit) {
        val getUserFolderData = cActivity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri ->
            callback(uri)
        }
        getUserFolderData.launch(filter.toUri())
    }

    fun saveFileRequester(cActivity: AppCompatActivity,filter : String,callback: (answer: Uri) -> Unit) {
        val getUserFolderData = cActivity.registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri ->
            callback(uri)
        }
        getUserFolderData.launch(filter.toUri())
    }

}

/*

    How to use.

myreq.messageRequester(this,
                       "Testing Ok!","with this message",
                       MessageRequester_Ok) { a ->

    Log.d("debug","$a")
}

myreq.messageRequester(this,
                       "Testing Yes / No","with this message",
                       MessageRequester_YesNo) { a ->

    Log.d("debug","$a")
}

myreq.messageRequester(this,
                       "Testing Yes/No Cancel!","with this message",
                       MessageRequester_YesNoCancel) { a ->

    Log.d("debug","$a")
}
*/

Upvotes: 0

Shlomi Katriel
Shlomi Katriel

Reputation: 2413

Assuming you use your application context (since you named it appContext) to show the dialog, this, I believe, is the root cause of the exception.

You should pass your current Activity context instead.

Change your function signature to ensure passing an activity:

fun messageRequester(activity: Activity, title: String, message: String, flags: Int) : Int {
...
}

Regarding the code blocking, it's only possible if you call messageRequester function on a background thread, and it's not the Android's native way.

You need to use some concurrency lock to lock the thread which called messageRequester and unlock it from the AlertDialog's buttons listeners, forwarding their answer by saving it into a variable.

Usually I don't recommend using locks since it's dangerous if you’re not familiar with concurrency principles.

Anyway, I recommend simply passing a lambda callback in the function and call it with the answer from buttons listeners.

Using Callback

// enum to hold the answer (you're free to use whatever type you want for callback)
enum class Answer { YES, NO, CANCEL }

// Function signature
fun messageRequester(activity: Activity, title: String, message: String, flags: Int, callback: (answer: Answer) -> Unit) {
...

}

// How to pass result to callback from buttons listener
callback(Answer.YES)

// Passing callback to function
myRequester.messageRequester(activity, message, flags) { answer ->
  // Handle answer here
}

Using Locks

// enum to hold the answer (you're free to use whatever type you want for callback)
enum class Answer { YES, NO, CANCEL }

// Function signature
fun messageRequester(activity: Activity, title: String, message: String, flags: Int) : Answer? {
  // Don't use main thread!!!!!!
  if (Looper.getMainLooper() == Looper.myLooper()) {
    throw IllegalStateException("Suspending main thread is forbidden!")
  }

  // Variable to hold the answer
  var answer: Answer? = null

  // The lock that holds the thread until an answer is given
  val countDownLatch = CountDownLatch(1)

  // How to pass answer from buttons listener (in AlertDialog creation)
  Listener {
    answer = Answer.YES
    countDownLatch.countDown()
  }
  
  // Wait for answer forever (not recommended since you can exit dialog without clicking buttons and block the thread forever)
  countDownLatch.await()

  // Wait for answer for 10 seconds (answer will be null in case of a timeout). returns false for timeouts.
  val handled = countDownLatch.await(10L, TimeOut.SECONDS)
  return answer 
}

Upvotes: 1

Related Questions