I am trying to get the icon of an application's notification. The listener works perfectly, but for most of the apps I tried (telegram, signal, android messages...) I cannot seem to get the app's icon.
Here is the code where I try to get the icon:
private fun getIcon(notification: StatusBarNotification, context: Context): Bitmap {
val packageManager = context.packageManager
return try {
Bitmap.createBitmap(drawableToBitmap(packageManager.getApplicationIcon(notification.packageName)))
} catch (exception: PackageManager.NameNotFoundException) {
return Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8)
}
}
private fun drawableToBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
if (drawable.bitmap != null) {
return drawable.bitmap
}
}
val bitmap: Bitmap = if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) {
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
} else {
Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
}
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
To get the icons you should use both class & package names instead of only using the package name. If the app supports changing icons or has more than one launcher activity this works much better.
fun getIcon(context: Context, packageName: String, className: String): Drawable? {
var drawable: Drawable? = null
try {
val intent = Intent(Intent.ACTION_MAIN)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.setClassName(packageName, className)
drawable = context.packageManager.getActivityIcon(intent)
} catch (e: Exception) {
try {
drawable = context.packageManager.getApplicationIcon(packageName)
} catch (e: Exception) {
e.printStackTrace()
}
}
return drawable
}
This function returns the icon drawable or returns null if it fails. It tries to get the icon using both package and class names at first but if it fails it uses only package name. You can just use the package name one if you don't want to use the class name as well.
If it's still just catching the exception every time and you know the app is installed, you probably lack some permissions. You can also try again with targetSdkVersion 29 instead of targetSdkVersion 30 since 30 adds some limitations to this kind of functions but I am not sure if those affect getting the icons as well.
Related
I am trying to capture photos in my app using standard camera app intent (I am NOT interested in using JetpackX or other library to have a viewfinder in my app).
When I had the code in my Fragment like so:
// This is in response to user clicking a button
fun startPhotoTaking() {
val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
resultLauncher.launch(takePictureIntent)
}
private var resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val photo = result.data?.extras?.get("data") as? Bitmap
photo?.let {
// ... do whatever
}
}
}
Then the photo Bitmap came back tiny as apparently Android caps intents at 1 Mb , but the orientation was correct.
Since I actually need the original large image, I have modified the code like so:
// This is in response to user clicking a button
fun startPhotoTaking() {
lastUri = getTmpFileUri()
if (lastUri != null) {
resultLauncher.launch(lastUri)
}
}
private fun getTmpFileUri(): Uri {
requireContext().cacheDir.listFiles()?.forEach { it.delete() }
val tmpFile = File
.createTempFile("tmp_image_file", ".jpg", requireContext().cacheDir)
.apply {
createNewFile()
}
return FileProvider.getUriForFile(
MyApplication.instance.applicationContext,
"${BuildConfig.APPLICATION_ID}.provider",
tmpFile
)
}
var lastUri: Uri? = null
private var resultLauncher =
registerForActivityResult(ActivityResultContracts.TakePicture()) { result ->
if (result) {
val photoUri = lastUri
if (photoUri != null) {
val stream = MyApplication.instance.applicationContext.contentResolver
.openInputStream(photoUri)
val photo = BitmapFactory.decodeStream(stream)
stream?.close()
// ... do whatever
// If i try ExifInterface(photoUri.path!!)
}
}
}
Now I do receive the actual large photo, but it is always landscape :(
I tried creating an instance of ExifInterface(photoUri.path) but that throws an exception for me (which I don't quite understand as I am only writing/reading to my own app's cache directory?):
java.io.FileNotFoundException: /cache/tmp_image_file333131952730914647.jpg: open failed: EACCES (Permission denied)
How can I get my photo to retain orientation when saved to file and/or get access to read EXIF parameters so I can rotate it myself?
Update
As a workaround, this did the trick but it's just... ungodly. So am very keen to find better solutions.
val stream = MyApplication.instance.applicationContext.contentResolver
.openInputStream(photoUri)
if (stream != null) {
val tempFile = File.createTempFile("tmp", ".jpg", requireContext().cacheDir)
.apply { createNewFile() }
val fos = FileOutputStream(tempFile)
val buf = ByteArray(8192)
var length: Int
while (stream.read(buf).also { length = it } > 0) {
fos.write(buf, 0, length)
}
fos.close()
val exif = ExifInterface(tempFile.path)
val orientation =
exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1)
val matrix = Matrix()
if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
matrix.postRotate(90f)
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
matrix.postRotate(180f)
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
matrix.postRotate(270f)
var photo = BitmapFactory.decodeFile(tempFile.path)
photo = Bitmap.createBitmap(
photo,
0,
0,
photo.width,
photo.height,
matrix,
true
)
}
ExifInterface(photoUri.path)
That isnot an existing path as you have seen.
Better use:
ExifInterface( tmpFile.getAbsolutePath())
You can rotate image to the particular angle like this,
// To rotate image
private fun rotateImage(source: Bitmap, angle: Float): Bitmap? {
val matrix = Matrix()
matrix.postRotate(angle)
return Bitmap.createBitmap(
source, 0, 0, source.width, source.height,
matrix, true
)
}
In your case set angle to 90f to get image in portrait orientation.
On some devices, if activity context is lost due to low memory when application was paused, blur effect on dialog background can not be recreated. In log such situation is related to PixelCopy.ERROR_SOURCE_NO_DATA error in OnPixelCopyFinishedListener. Previously, in that case we just trying to restart pixel copying. When I debugging blur creation, it always successfully recreated. So, for now I added conditional delay, which is activated, when ERROR_SOURCE_NO_DATA is received. But, maybe, this error must be handled in the different way?
#RequiresApi(Build.VERSION_CODES.O)
private class PixelCopyTask(
val window: Window,
val srcRect: Rect,
val dstViewWeak: WeakReference<View>,
val captureViewBitmap: Bitmap,
val scaleFactor: Float,
val radius: Int,
val startTime: Long
) : PixelCopy.OnPixelCopyFinishedListener {
var wasSourceNoDataError = false
fun start() {
try {
PixelCopy.request(window, srcRect, captureViewBitmap, this, uiHandler)
} catch (e: IllegalArgumentException) {
Timber.d("BlurViewBitmapFactory: PixelCopy fail create screen capture")
}
}
override fun onPixelCopyFinished(copyResult: Int) {
val dst = dstViewWeak.get() ?: return
checkTime(startTime) { return }
when (copyResult) {
PixelCopy.SUCCESS -> setBlurBitmapBackground(captureViewBitmap, dst, scaleFactor, radius, startTime, wasSourceNoDataError)
PixelCopy.ERROR_SOURCE_NO_DATA -> {
wasSourceNoDataError = true
Timber.d("PixelCopy.ERROR_SOURCE_NO_DATA")
start()
}
else -> return
}
}
}
I need to get the screenshot of a View. I have tried two methods to do this work. Unfortunately, both result in the same bug.
Here is the log:
java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps
at android.graphics.BaseCanvas.null onHwBitmapInSwMode(null)(BaseCanvas.java:550)
at android.graphics.BaseCanvas.null throwIfHwBitmapInSwMode(null)(BaseCanvas.java:557)
at android.graphics.BaseCanvas.null throwIfCannotDraw(null)(BaseCanvas.java:69)
at android.graphics.BaseCanvas.null drawBitmap(null)(BaseCanvas.java:127)
at android.graphics.Canvas.null drawBitmap(null)(Canvas.java:1504)
at android.graphics.drawable.BitmapDrawable.null draw(null)(BitmapDrawable.java:545)
at android.widget.ImageView.null onDraw(null)(ImageView.java:1355)
at android.view.View.null draw(null)(View.java:20248)
at android.view.View.null draw(null)(View.java:20118)
at android.view.ViewGroup.null drawChild(null)(ViewGroup.java:4336)
at android.view.ViewGroup.null dispatchDraw(null)(ViewGroup.java:4115)
at android.view.ViewOverlay$OverlayViewGroup.null dispatchDraw(null)(ViewOverlay.java:251)
at android.view.View.null draw(null)(View.java:20251)
at android.view.View.null buildDrawingCacheImpl(null)(View.java:19516)
at android.view.View.null buildDrawingCache(null)(View.java:19379)
at android.view.View.null getDrawingCache(null)(View.java:19215)
at android.view.View.null getDrawingCache(null)(View.java:19166)
at com.omnipotent.free.videodownloader.pro.utils.ViewUtils.android.graphics.Bitmap captureView(android.view.View)(ViewUtils.java:70)
at com.omnipotent.free.videodownloader.pro.ui.main.MainActivity.com.omnipotent.free.videodownloader.pro.data.bean.TabBean getCurrentTabsData()(MainActivity.java:325)
at com.omnipotent.free.videodownloader.pro.ui.main.MainActivity.com.omnipotent.free.videodownloader.pro.data.bean.TabBean access$getCurrentTabsData(com.omnipotent.free.videodownloader.pro.ui.main.MainActivity)(MainActivity.java:84)
at com.omnipotent.free.videodownloader.pro.ui.main.MainActivity$onAddTab$1.void run()(MainActivity.java:628)
at android.os.Handler.null handleCallback(null)(Handler.java:873)
at android.os.Handler.null dispatchMessage(null)(Handler.java:99)
at android.os.Looper.null loop(null)(Looper.java:193)
at android.app.ActivityThread.null main(null)(ActivityThread.java:6936)
at java.lang.reflect.Method.null invoke(null)(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.null run(null)(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.null main(null)(ZygoteInit.java:870)
I have examined the code carefully and looked up related articles on the Internet. I, however, have not solved it yet, which really make me feel frustrating.
This bug has only happened above android O.
Here are two methods that I have tried:
Method1:
public static Bitmap captureView(View view) {
Bitmap tBitmap = Bitmap.createBitmap(
view.getWidth(), view.getHeight(), Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(tBitmap);
view.draw(canvas);
canvas.setBitmap(null);
return tBitmap;
}
Method2:
public static Bitmap captureView(View view) {
if (view == null) return null;
boolean drawingCacheEnabled = view.isDrawingCacheEnabled();
boolean willNotCacheDrawing = view.willNotCacheDrawing();
view.setDrawingCacheEnabled(true);
view.setWillNotCacheDrawing(false);
final Bitmap drawingCache = view.getDrawingCache();
Bitmap bitmap;
if (null == drawingCache) {
view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
view.buildDrawingCache();
bitmap = Bitmap.createBitmap(view.getDrawingCache());
} else {
bitmap = Bitmap.createBitmap(drawingCache);
}
view.destroyDrawingCache();
view.setWillNotCacheDrawing(willNotCacheDrawing);
view.setDrawingCacheEnabled(drawingCacheEnabled);
return bitmap;
}
What need to be mentioned is that I have set android:hardwareAccelerated="true" for my Activity, where I invoked captureView method.
Read Glide hardware bitmap docs, and find PixelCopy class, which may solve this bug.
Use PixelCopy to convert view to Bitmap above Android O, and use previous method below Android O.
Here is my code:
fun captureView(view: View, window: Window, bitmapCallback: (Bitmap)->Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Above Android O, use PixelCopy
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
val location = IntArray(2)
view.getLocationInWindow(location)
PixelCopy.request(window,
Rect(location[0], location[1], location[0] + view.width, location[1] + view.height),
bitmap,
{
if (it == PixelCopy.SUCCESS) {
bitmapCallback.invoke(bitmap)
}
},
Handler(Looper.getMainLooper()) )
} else {
val tBitmap = Bitmap.createBitmap(
view.width, view.height, Bitmap.Config.RGB_565
)
val canvas = Canvas(tBitmap)
view.draw(canvas)
canvas.setBitmap(null)
bitmapCallback.invoke(tBitmap)
}
}
The shortage is that I have to use a callback, which I don't like very much.
Hope that it works.
you can try copying hardware type bitmap to your desired type bitmap,e.g:
Bitmap targetBmp = originBmp.copy(Bitmap.Config.ARGB_8888, false);
Based on #wang willway response, I created this Kotlin extension of View.
Use it in Compose as:
LocalView.current.toBitmap(
onBitmapReady = { bitmap: Bitmap ->
// TODO - use generated bitmap
},
onBitmapError = { exception: Exception ->
// TODO - handle exception
}
)
// start of extension.
fun View.toBitmap(onBitmapReady: (Bitmap) -> Unit, onBitmapError: (Exception) -> Unit) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val temporalBitmap = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888)
// Above Android O, use PixelCopy due
// https://stackoverflow.com/questions/58314397/
val window: Window = (this.context as Activity).window
val location = IntArray(2)
this.getLocationInWindow(location)
val viewRectangle = Rect(location[0], location[1], location[0] + this.width, location[1] + this.height)
val onPixelCopyListener: PixelCopy.OnPixelCopyFinishedListener = PixelCopy.OnPixelCopyFinishedListener { copyResult ->
if (copyResult == PixelCopy.SUCCESS) {
onBitmapReady(temporalBitmap)
} else {
error("Error while copying pixels, copy result: $copyResult")
}
}
PixelCopy.request(window, viewRectangle, temporalBitmap, onPixelCopyListener, Handler(Looper.getMainLooper()))
} else {
val temporalBitmap = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.RGB_565)
val canvas = android.graphics.Canvas(temporalBitmap)
this.draw(canvas)
canvas.setBitmap(null)
onBitmapReady(temporalBitmap)
}
} catch (exception: Exception) {
onBitmapError(exception)
}
}
If you are converting a view that has an ImageView in it and you are loading its image using an Image loading library like Glide or coil then you have to disable the use of hardware bitmaps. These libraries store pixel data only in graphics memory and are optimal for cases where the Bitmap is only drawn to the screen.
If you are using coil then you have to do something like this
val imageLoader = ImageLoader.Builder(context).allowHardware(false).build()
imageView.load(imageUrl, imageLoader)
I used it for screenshoting Composables but you can use this without Compose. It returns every exception you might counter as wrapped error inside ImageResult.
fun View.screenshot( bitmapCallback: (ImageResult) -> Unit) {
try {
val bitmap = Bitmap.createBitmap(
width,
height,
Bitmap.Config.ARGB_8888,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PixelCopy.request(
(this.context as Activity).window,
bounds.toAndroidRect(),
bitmap,
{
when (it) {
PixelCopy.SUCCESS -> {
bitmapCallback.invoke(ImageResult.Success(bitmap))
}
PixelCopy.ERROR_DESTINATION_INVALID -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"The destination isn't a valid copy target. " +
"If the destination is a bitmap this can occur " +
"if the bitmap is too large for the hardware to " +
"copy to. " +
"It can also occur if the destination " +
"has been destroyed"
)
)
)
}
PixelCopy.ERROR_SOURCE_INVALID -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"It is not possible to copy from the source. " +
"This can happen if the source is " +
"hardware-protected or destroyed."
)
)
)
}
PixelCopy.ERROR_TIMEOUT -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"A timeout occurred while trying to acquire a buffer " +
"from the source to copy from."
)
)
)
}
PixelCopy.ERROR_SOURCE_NO_DATA -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"The source has nothing to copy from. " +
"When the source is a Surface this means that " +
"no buffers have been queued yet. " +
"Wait for the source to produce " +
"a frame and try again."
)
)
)
}
else -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"The pixel copy request failed with an unknown error."
)
)
)
}
}
},
Handler(Looper.getMainLooper())
)
} else {
val canvas = Canvas(bitmap)
.apply {
translate(-bounds.left, -bounds.top)
}
this.draw(canvas)
canvas.setBitmap(null)
bitmapCallback.invoke(ImageResult.Success(bitmap))
}
} catch (e: Exception) {
bitmapCallback.invoke(ImageResult.Error(e))
}
}
sealed class ImageResult {
object Initial : ImageResult()
data class Error(val exception: Exception) : ImageResult()
data class Success(val data: Bitmap) : ImageResult()
}
Usage
This may help somebody in future In my case It was due to giving fixed height to rating bar and at the same time setting style as below
style="#style/Widget.AppCompat.RatingBar.Small"
What worked for me is just wrapping the height of it instead of giving it fixed size.
Use this to capture a view (Works on android 8.0):
class CaptureView{
static Bitmap capture(View view){
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
return view.getDrawingCache(); //Returns a bitmap
}
}
Bitmap bitmap = CaptureView.capture(yourView) // Pass in the view to capture
I'm particularly interested in high contrast text, color correction, and magnification settings. I did some research online, couldn't find what I want. I saw one answer about detecting high contrast text:
AccessibilityManager am = (AccessibilityManager) this.getSystemService(Context.ACCESSIBILITY_SERVICE);
boolean isHighTextContrastEnabled = am.isHighTextContrastEnabled();
But somehow it gives me the error for isHighTextContrastEnabled() saying that it is undefined for the type AccessibilityManager.
Also didn't find solution for the other two settings detection.
AccessibilityManager am = (AccessibilityManager) this.getSystemService(Context.ACCESSIBILITY_SERVICE);
Class clazz = am.getClass();
Method m = null;
try {
m = clazz.getMethod("isHighTextContrastEnabled",null);
} catch (NoSuchMethodException e) {
Log.w("FAIL", "isHighTextContrastEnabled not found in AccessibilityManager");
}
Object result = null;
try {
result = m.invoke(am, null);
if (result != null && result instanceof Boolean) {
Boolean b = (Boolean)result;
Log.d("result", "b =" + b);
}
} catch (Exception e) {
android.util.Log.d("fail", "isHighTextContrastEnabled invoked with an exception" + e.getMessage());
return;
}
and I do test, it return false, so it works
What a did was
private fun checkForAcessibility(): Boolean {
try {
val accessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
val accessibilityManagerClass = accessibilityManager.javaClass
val isHighTextContrastEnabledMethod = accessibilityManagerClass.getMethod("isHighTextContrastEnabled")
val result: Any = isHighTextContrastEnabledMethod.invoke(accessibilityManager) ?: return AccessibilityEnabledValue.ERROR_QUERYING_VALUE
if (result !is Boolean) {
return AccessibilityEnabledValue.ERROR_QUERYING_VALUE
}
return if (result) {
AccessibilityEnabledValue.TRUE
} else {
AccessibilityEnabledValue.FALSE
}
} catch (e: Exception) {
return AccessibilityEnabledValue.ERROR_QUERYING_VALUE
}
}
enum class AccessibilityEnabledValue(val value: String) {
TRUE("true"),
FALSE("false"),
ERROR_QUERYING_VALUE("error_querying_value")
}
I've noticed that isHighTextContrastEnabled() method does not contain parameters.
use this for color correction:
int color_correction_enabled = 0;
try {
color_correction_enabled = Settings.Secure.getInt(this.getContentResolver(), "accessibility_display_daltonizer_enabled");
} catch (Exception e) {
color_correction_enabled = 0; // means default false
}
Though reflection solution helps with some devices I was unable to get "isHighTextContrastEnabled" via reflection on my OnePlus 5 device.
So I ended up with another method when reflection fails. Just rendering text on a bitmap and then manually checking if there is a specific color (different from black and white) in the bitmap (cause in "High Contrast" mode it will be black and white).
Here is the code in Kotlin:
private fun isHighTextContrastEnabledWithBmp(): Boolean {
val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
val testColor = Color.GREEN
val paint = Paint().apply {
color = testColor
textSize = 5f
}
canvas.drawText("1", 0f, 10f, paint)
val pixels = IntArray(bmp.width * bmp.height)
bmp.getPixels(
pixels, 0, bmp.width,
0, 0, bmp.height, bmp.width
)
val result = pixels.any { it == testColor }.not()
return result
}
Using androidx.core:core-ktx's getSystemService extension,
isHighTextContrastEnabled = runCatching {
context.getSystemService<AccessibilityManager>()?.let {
(it.javaClass.getMethod("isHighTextContrastEnabled").invoke(it) as? Boolean)
}
}.onFailure { it.printStackTrace() }.getOrNull() ?: false
I have a button in action bar, for which the icon is changed depending of a boolean. I would like to check which drawable resource is used.
Here is the code where the icon is changed:
#Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
MenuItem item = menu.findItem(R.id.menu_favorite);
if(mIsFavorite)
item.setIcon(R.drawable.ab_icon_on);
else
item.setIcon(R.drawable.ab_icon_off);
}
When icon needs to be changed, the menu is invalidated:
// request menu update
supportInvalidateOptionsMenu();
Finally, my espresso code where I would like to check the result:
#Test
public void action_setUnsetFavorite() {
// check favorite off
onView(withImageDrawable(R.drawable.ab_icon_off))
.check(matches(isDisplayed()));
// click favorite button
onView(withId(R.id.menu_favorite))
.perform(click());
// check favorite on
onView(withImageDrawable(R.drawable.ab_icon_on))
.check(matches(isDisplayed()));
Please note that I am using a custom matcher found here.
I'm not a 100% sure on how the matchers work and whether this is the best response but using a slightly different version of the method certainly works.
The problem is that the current matcher only works with ImageViews. ActionMenuItemView actually subclasses textView so won't match and it also has no method for getDrawable().
Please note, this still requires the sameBitmap method from the original post.
public static Matcher<View> withActionIconDrawable(#DrawableRes final int resourceId) {
return new BoundedMatcher<View, ActionMenuItemView>(ActionMenuItemView.class) {
#Override
public void describeTo(final Description description) {
description.appendText("has image drawable resource " + resourceId);
}
#Override
public boolean matchesSafely(final ActionMenuItemView actionMenuItemView) {
return sameBitmap(actionMenuItemView.getContext(), actionMenuItemView.getItemData().getIcon(), resourceId);
}
};
}
The answer by #Barry Irvine is really helpful. Just wanted to clarify if anyone wonders how to use the given Matcher method in an Espresso test. (Kotlin)
Step 1: Create a new file CustomMatchers from this link mentioned in the question.
(include the sameBitmap method, however, looking at the comments, modified the sameBitmap method)
You could directly add the method within your test file, but adding it in a different file would help in re-using it whenever you need to test menu item icons.
For reference, this is how my CustomMatchers file looks like
object CustomMatchers {
fun withActionIconDrawable(#DrawableRes resourceId: Int): Matcher<View?> {
return object : BoundedMatcher<View?, ActionMenuItemView>(ActionMenuItemView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has image drawable resource $resourceId")
}
override fun matchesSafely(actionMenuItemView: ActionMenuItemView): Boolean {
return sameBitmap(
actionMenuItemView.context,
actionMenuItemView.itemData.icon,
resourceId,
actionMenuItemView
)
}
}
}
private fun sameBitmap(
context: Context,
drawable: Drawable?,
resourceId: Int,
view: View
): Boolean {
var drawable = drawable
val otherDrawable: Drawable? = context.resources.getDrawable(resourceId)
if (drawable == null || otherDrawable == null) {
return false
}
if (drawable is StateListDrawable) {
val getStateDrawableIndex =
StateListDrawable::class.java.getMethod(
"getStateDrawableIndex",
IntArray::class.java
)
val getStateDrawable =
StateListDrawable::class.java.getMethod(
"getStateDrawable",
Int::class.javaPrimitiveType
)
val index = getStateDrawableIndex.invoke(drawable, view.drawableState)
drawable = getStateDrawable.invoke(drawable, index) as Drawable
}
val bitmap = getBitmapFromDrawable(context, drawable)
val otherBitmap = getBitmapFromDrawable(context, otherDrawable)
return bitmap.sameAs(otherBitmap)
}
private fun getBitmapFromDrawable(context: Context?, drawable: Drawable): Bitmap {
val bitmap: Bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
} }
Step 2: Using the matcher in a test
onView(withId(R.id.menu_item_id))
.check(matches(CustomMatchers.withActionIconDrawable(R.drawable.ic_favorite_border)))