Simplify Bug Reporting: Using ClickUp to Enhance Your App Development

Aaush Shrestha
8 min readSep 11, 2023

--

While developing software, sometimes things don’t work quite right.
Imagine you’re building a project, and QA or your client finds a problem. They usually have to spend much time telling you what’s wrong.
But what if there was a faster way? That’s where this solution comes in. We’re going to make a special tool that makes reporting problems easy. It can take pictures of the issues, let you draw on the images to show what’s wrong, and then send all the information directly to ClickUp, a tool that helps manage projects.

Step-by-Step Guide: Building a Bug Reporting Tool with ClickUp Integration

  1. Create a floating view for capturing screenshots.
  2. Design a user-friendly input layout for bug descriptions.
  3. Implement an image editing feature, allowing you to draw on screenshots.
  4. Set up your ClickUp personal token and workspace.
  5. Learn how to upload bug reports directly to ClickUp tasks.

Step 1, Create a floating view

class ReportLayout : FrameLayout {

private var dX = 0f
private var dY = 0f
var bitmap: Bitmap? = null
var isMoving = false


fun captureScreenshot(context: Context): String? {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)

val imageFileName = "screenshot_$timeStamp.jpg"
val imageFile = File(storageDir, imageFileName)

try {
val rootView = (context as Activity).window.decorView
rootView.isDrawingCacheEnabled = true
val bitmap = Bitmap.createBitmap(rootView.drawingCache)
rootView.isDrawingCacheEnabled = false

val fos = FileOutputStream(imageFile)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
fos.flush()
fos.close()

return imageFile.absolutePath
} catch (e: Exception) {
e.printStackTrace()
}

return null
}

val gestureDetector =
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
this@ReportLayout.isVisible = false
val imagePath = captureScreenshot(context) ?: ""
val intent = Intent(context, InputActivity::class.java)
intent.putExtra("imagePath", imagePath)

context.startActivity(intent)
this@ReportLayout.isVisible = true

return true
}
})

constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)

init {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
isClickable = true
isFocusable = true
elevation = 300f

setOnTouchListener { v, event ->
gestureDetector.onTouchEvent(event)
when (event?.actionMasked) {
MotionEvent.ACTION_DOWN -> {
dX = v.x - event.rawX
dY = v.y - event.rawY
isMoving = false
}

MotionEvent.ACTION_MOVE -> {
val newX = event.rawX + dX
val newY = event.rawY + dY
if (!isMoving) {
isMoving = true
}
v.x = newX
v.y = newY
}

MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (!isMoving) {
// Handle click event here
} else {
val newX = v.x
val newY = v.y
val nearestEdge = calculateNearestEdge(newX, newY, v.width, v.height)

when (nearestEdge) {
NearestEdge.LEFT -> v.x = 0f
NearestEdge.RIGHT -> v.x =
(v.context.resources.displayMetrics.widthPixels - v.width).toFloat()
NearestEdge.TOP -> v.y = 0f
NearestEdge.BOTTOM -> v.y =
(v.context.resources.displayMetrics.heightPixels - v.height).toFloat()
}
}
}

}
true
}

val shapeableImageView = ShapeableImageView(context)

val layoutParams = ViewGroup.LayoutParams(
180,
180
)
shapeableImageView.layoutParams = layoutParams
shapeableImageView.scaleType = ImageView.ScaleType.FIT_XY
val shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(CornerFamily.ROUNDED, 100f)
.build()

shapeableImageView.shapeAppearanceModel = shapeAppearanceModel
shapeableImageView.setImageResource(R.mipmap.ic_launcher)

this.addView(shapeableImageView)

}

enum class NearestEdge {
LEFT, RIGHT, TOP, BOTTOM
}

fun calculateNearestEdge(x: Float, y: Float, width: Int, height: Int): NearestEdge {
val screenWidth = resources.displayMetrics.widthPixels
val screenHeight = resources.displayMetrics.heightPixels

val centerX = x + width / 2
val centerY = y + height / 2

val distances = listOf(
centerX,
screenWidth - centerX,
centerY,
screenHeight - centerY
)

val minDistance = distances.minOrNull() ?: 0f

return when (minDistance) {
distances[0] -> NearestEdge.LEFT
distances[1] -> NearestEdge.RIGHT
// distances[2] -> NearestEdge.TOP
//distances[3] -> NearestEdge.BOTTOM
else -> NearestEdge.LEFT
}
}
}

The provided code creates a floating icon on your app’s screen. When you double-tap this icon, it captures a screenshot of your current screen and then takes you to the next step in your bug-reporting process.

Now you need to add this view on your base activity so that the tester (client or QA) can access this from all over the app.

    // Initialize reportLayout 
private val reportLayout: ReportLayout by lazy { ReportLayout(this) }
// add layout in root view inside onCreate method
val rootLayout: ViewGroup = findViewById(android.R.id.content)
rootLayout.addView(reportLayout)

Step 2, Design input layout
In this step, our objective is to create an efficient input layout that facilitates bug reporting from testers. In the context of ClickUp, it’s crucial to note that both the bug title and description are mandatory fields. Therefore, our design aims to meet these requirements seamlessly. Below is the sample code for implementing this optimized input layout and code to show the captured screenshot.

class InputActivity : AppCompatActivity() {
private lateinit var binding: ActivityInputBinding
var imagePath: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityInputBinding.inflate(layoutInflater)
setContentView(binding.root)
imagePath = intent.getStringExtra("imagePath")
val bitmap = BitmapFactory.decodeFile(imagePath)
if (bitmap != null) {
binding.bugScreenshotImageView.setImageBitmap(bitmap)
}
binding.bugDescriptionEditText.setText("hello honey")

findViewById<Button>(R.id.reportButton).setOnClickListener {
val listOfTag = binding.tagsEditText.text.toString().split(",").toList()

ClickUpTaskCreator.createTask(
this,
taskName = binding.bugTitleEditText.text.toString(),
taskDescription = binding.bugDescriptionEditText.text.toString(),
tags = listOfTag,
imageFile = Uri.parse(imagePath)
)
finish()
}

binding.editImage.setOnClickListener {
val intent = Intent(this, DrawActivity::class.java)
intent.putExtra("imagePath", imagePath)
resultLauncher.launch(intent)
}
}

var resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
imagePath = data?.getStringExtra("returnImage")
val bitmap = BitmapFactory.decodeFile(imagePath)
binding.bugScreenshotImageView.setImageBitmap(bitmap)
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".reportTool.InputActivity">


<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:id="@+id/cancel"
android:padding="12dp"
android:text="Cancel"
android:textSize="18sp"
android:textStyle="bold" />




<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Enter Bug Title">


<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bugTitleEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Enter Bug Description">


<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bugDescriptionEditText"
android:layout_width="match_parent"

android:layout_height="150dp" />

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Enter Tags (comma separated)">


<com.google.android.material.textfield.TextInputEditText
android:id="@+id/tagsEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Enter Priority">


<com.google.android.material.textfield.TextInputEditText
android:id="@+id/priorityEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</com.google.android.material.textfield.TextInputLayout>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Bug Screenshot:"
android:textSize="18sp"
android:textStyle="bold" />


<ImageView
android:id="@+id/bugScreenshotImageView"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginTop="8dp"
android:scaleType="fitCenter" />


<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:layout_marginTop="8dp"
android:gravity="center"
android:id="@+id/editImage"
android:text="Edit Image"
android:textSize="18sp"
android:textStyle="bold" />


<androidx.appcompat.widget.AppCompatButton
android:id="@+id/reportButton"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Report" />

</LinearLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

Step 3, Implement an image editing feature
During this step, we pass the captured screenshot to a new activity to draw and paint directly on the image to highlight the bug. Upon completion, the edited image is returned, providing a visually enhanced bug report for further analysis and action.

Here is the sample code to edit the image

First, you need to create a custom view in which we can use canvas.

class DrawableImageView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) {

var BRUSH_SIZE = 10
val DEFAULT_COLOR = Color.BLACK
val DEFAULT_BG_COLOR = Color.WHITE
private val TOUCH_TOLERANCE = 4f
private val backgroundColor: Int = DEFAULT_BG_COLOR
private var mX = 0f
private var mY = 0f
private var mPath = Path()
private var mPaint = Paint()
private var paths = ArrayList<FingerPath>()
private var currentColor = 0
private var strokeWidth = 0
private var emboss = false
private var blur = false
private val mEmboss: MaskFilter
private val mBlur: MaskFilter
private var mBitmap: Bitmap? = null
private var mCanvas: Canvas? = null
private val mBitmapPaint = Paint(Paint.DITHER_FLAG)
private var backgroundImage: Bitmap? = null
private val backgroundPaint = Paint()

init {
mPaint = Paint()
mPaint.isAntiAlias = true
mPaint.isDither = true
mPaint.color = DEFAULT_COLOR
mPaint.style = Paint.Style.STROKE
mPaint.strokeJoin = Paint.Join.ROUND
mPaint.strokeCap = Paint.Cap.ROUND
mPaint.xfermode = null
mPaint.alpha = 0xff

mEmboss = EmbossMaskFilter(floatArrayOf(1f, 1f, 1f), 0.4f, 6F, 3.5f)
mBlur = BlurMaskFilter(5F, BlurMaskFilter.Blur.NORMAL)
}

fun initSignature(metrics: Point, bitmap: Bitmap) {
val height = metrics.y
val width = metrics.x
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
mCanvas = Canvas(mBitmap!!)
currentColor = DEFAULT_COLOR
strokeWidth = BRUSH_SIZE
backgroundImage = bitmap
mCanvas?.drawBitmap(bitmap, 0f, 0f, backgroundPaint)
}

fun normal() {
emboss = false
blur = false
}

fun getSignature(): File? {
return getBitMapPath(context, mBitmap!!)
}

override fun onDraw(canvas: Canvas) {
canvas.save()
for (fp in paths) {
mPaint.color = fp.color
mPaint.strokeWidth = fp.strokeWidth.toFloat()
mPaint.maskFilter = null
if (fp.emboss) mPaint.maskFilter = mEmboss else if (fp.blur) mPaint.maskFilter = mBlur
mCanvas!!.drawPath(fp.path, mPaint)
}
canvas.drawBitmap(mBitmap!!, 0f, 0f, mBitmapPaint)
canvas.restore()
}

private fun touchStart(x: Float, y: Float) {
mPath = Path()
val fp = FingerPath(currentColor, emboss, blur, strokeWidth, mPath!!)
paths.add(fp)
mPath.reset()
mPath.moveTo(x, y)
mX = x
mY = y
}

private fun touchMove(x: Float, y: Float) {
val dx = Math.abs(x - mX)
val dy = Math.abs(y - mY)
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath!!.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2)
mX = x
mY = y
}
}

private fun touchUp() {
mPath!!.lineTo(mX, mY)
}

override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x
val y = event.y
when (event.action) {
MotionEvent.ACTION_DOWN -> {
touchStart(x, y)
invalidate()
}
MotionEvent.ACTION_MOVE -> {
touchMove(x, y)
invalidate()
}
MotionEvent.ACTION_UP -> {
touchUp()
invalidate()
}
}
return true
}

companion object {
var BRUSH_SIZE = 20
const val DEFAULT_COLOR = Color.RED
const val DEFAULT_BG_COLOR = Color.WHITE
private const val TOUCH_TOLERANCE = 4f
}
}


class FingerPath(
var color: Int,
var emboss: Boolean,
var blur: Boolean,
var strokeWidth: Int,
var path: Path
)
fun getBitMapPath(context: Context, bitmap: Bitmap): File {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)

val imageFileName = "screenshot_$timeStamp.jpg"
val imageFile = File(storageDir, imageFileName)

imageFile.createNewFile()
val bos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
val bitmapdata = bos.toByteArray()

var fos: FileOutputStream? = null
try {
fos = FileOutputStream(imageFile)
} catch (e: FileNotFoundException) {
e.printStackTrace()
}
try {
fos!!.write(bitmapdata)
fos.flush()
fos.close()
} catch (e: IOException) {
e.printStackTrace()
}
return imageFile
}

Now add this view on an activity to access and draw on the image.

class DrawActivity : AppCompatActivity() {
private lateinit var binding: ActivityDrawBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDrawBinding.inflate(layoutInflater)
setContentView(binding.root)

val imagePath = intent.getStringExtra("imagePath")
val bitmap = BitmapFactory.decodeFile(imagePath)
println("DrawActivity:$imagePath")
binding.drawableImageView.initSignature(currentWindowMetricsPointCompat(),bitmap)
binding.done.setOnClickListener {
val returnImage= binding.drawableImageView.getSignature().toString()
println("returnImage:$returnImage")
val returnIntent = Intent()
returnIntent.putExtra("returnImage", returnImage)
setResult(Activity.RESULT_OK, returnIntent)
finish()
}
}

private fun currentWindowMetricsPointCompat(): Point {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val windowInsets = windowManager.currentWindowMetrics.windowInsets
var insets: Insets = windowInsets.getInsets(WindowInsets.Type.navigationBars())
windowInsets.displayCutout?.run {
insets = Insets.max(insets, Insets.of(safeInsetLeft, safeInsetTop, safeInsetRight, safeInsetBottom))
}
val insetsWidth = insets.right + insets.left
val insetsHeight = insets.top + insets.bottom
Point(windowManager.currentWindowMetrics.bounds.width() - insetsWidth, windowManager.currentWindowMetrics.bounds.height() - insetsHeight)
}else{
Point().apply {
windowManager.defaultDisplay.getSize(this)
}
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<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"
tools:context=".reportTool.DrawActivity">



<TextView
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
android:text="Cancel"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<TextView
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="12dp"
android:text="Done"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<com.abc.clickupapp.reportTool.DrawableImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/drawableImageView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/done" />


</androidx.constraintlayout.widget.ConstraintLayout>

In this particular segment of code, we have created a custom view that serves the purpose of displaying the screenshot captured in the previous step. This view extends functionality by enabling users to engage in drawing and annotation activities directly on the displayed image and returns the edited image to your Inout activity and you can proceed further for API integration.

Step 4, Set up your ClickUp personal token and workspace.
To create a personal token, Follow these steps.

  1. Log into ClickUp.
  2. Click on your avatar in the lower-left corner and select Apps.
  3. Under API Token, click Regenerate.
  4. Your new API token is available immediately!

To create a workspace ID,
1. Create space
2. Hover the space folder and click the plus icon
3. Create a new folder
4. Get id from the URL (last id after “/”)

Also for more information, you can follow this official documentation here.

Step 5, Create task on click up via API
You can get all the API information from here.

First, Create a task using this clickup endpoint.
https://api.clickup.com/api/v2/list/{$KEY_SPACE_ID}/task
After creating a task using this API you will get a bunch of information as a return but you will need only “id” from the response to upload the image.
Now, grab the ID from the response and upload the image using this endpoint.
https://api.clickup.com/api/v2/task/{$TASK_ID}/attachment
The image should be uploaded in multipart.

add ClickUp personal token as header Authentication
val requestBuilder = original.newBuilder()
.header(“Authorization”, API_KEY)

Here is the GitHub link for more implementation reference.

Conclusion

By empowering testers and developers with a simplified bug-reporting workflow, this tool represents a significant step towards improving the quality and efficiency of software development projects. As the software development landscape continues to evolve, having such a tool in your arsenal can make a substantial difference in delivering high-quality, bug-free software to your clients and end-users.

Thank you for reading, Happy coding NAMASTE.
Also thanks for the idea to my colleague.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Aaush Shrestha
Aaush Shrestha

No responses yet

Write a response