<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Leonardo Colman Lopes</title>
	<atom:link href="https://leonardo.colman.com.br/feed/" rel="self" type="application/rss+xml" />
	<link>https://leonardo.colman.com.br</link>
	<description>Sítio web de um simples amante da vida</description>
	<lastBuildDate>Mon, 10 Mar 2025 21:03:28 +0000</lastBuildDate>
	<language>pt-BR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.8.3</generator>
	<item>
		<title>Expand your digital picture frame use cases with Pi3D integrated USB media stick support using UDiskie</title>
		<link>https://dev.to/leocolman/expand-your-digital-picture-frame-use-cases-with-pi3d-integrated-usb-media-stick-support-using-2737</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Mon, 10 Mar 2025 21:03:28 +0000</pubDate>
				<category><![CDATA[Sem categoria]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2025/03/10/expand-your-digital-picture-frame-use-cases-with-pi3d-integrated-usb-media-stick-support-using-2737/</guid>

					<description><![CDATA[I built and customized three instances of The Digital Picture Frame. It’s so simple yet such a great system setup that I fell in love. Today I want to share a method to use USB Media Sticks to view our photos. This method uses Udiskie, while the traditional method (explained by Wolfgang Männel in Expand <a class="read-more" href="https://dev.to/leocolman/expand-your-digital-picture-frame-use-cases-with-pi3d-integrated-usb-media-stick-support-using-2737">Read more</a>]]></description>
										<content:encoded><![CDATA[<div>
<p>I built and customized three instances of <a href="https://www.thedigitalpictureframe.com/how-to-build-the-best-raspberry-pi-digital-picture-frame-with-bookworm-wayland-2025-edition-pi-2-3-4-5/" rel="noopener noreferrer">The Digital Picture Frame</a>. It’s so simple yet such a great system setup that I fell in love.</p>
<p>Today I want to share a method to use USB Media Sticks to view our photos. This method uses <code>Udiskie</code>, while the traditional method (explained by Wolfgang Männel in <a href="https://www.thedigitalpictureframe.com/expand-your-digital-picture-frame-use-cases-with-pi3d-integrated-usb-media-stick-support/" rel="noopener noreferrer">Expand your digital picture frame use cases with Pi3D integrated USB media stick support</a>) uses python scripts to achieve a very similar goal.</p>
<p>I recommend reading both articles and picking the method you feel more comfortable in. Both methods are mostly feature compatible in the end &#8212; having an auto-mounted USB device.</p>
<h2>
<p>  What This Setup Does<br />
</h2>
<ul>
<li>Use <a href="https://github.com/coldfix/udiskie?tab=readme-ov-file" rel="noopener noreferrer">Udiskie</a> to manage USB auto-loading/reloading</li>
<li>Modify Picture Frame&#8217;s configuration to read photos from the automount directory</li>
<li>Prepare the system to have <code>Udiskie</code> running when the system boots</li>
</ul>
<h2>
<p>  Installing Udiskie<br />
</h2>
<blockquote>
<p>udiskie is a udisks2 front-end that allows to manage removable media such as CDs or flash drives from userspace.</p>
</blockquote>
<p>Let&#8217;s install <code>udiskie</code> by running one simple command:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>sudo apt install udiskie udisks2
</code></pre>
</div>
<p>And although we won&#8217;t use it, udiskie has a cool UI!</p>
<p><a href="https://leonardo.colman.com.br/wp-content/uploads/2025/03/g2poq6jkzcseok22p54i.png" class="article-body-image-wrapper"><img fetchpriority="high" decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/03/g2poq6jkzcseok22p54i.png" alt="udiskie interface" width="750" height="340"></a></p>
<h2>
<p>  Setup Udiskie Auto-Start<br />
</h2>
<p>To setup udiskie to work without password prompt and on boot we must first install PolicyKit:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>sudo apt-get install policykit-1
</code></pre>
</div>
<p>And then let&#8217;s create a policy configuration with required permissions. Let&#8217;s create the file with <code>sudo nano /etc/polkit-1/rules.d/99-udisks2.rules</code> and the following content:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>polkit.addRule(function(action, subject) {
    if (action.id.indexOf("org.freedesktop.udisks2.filesystem-mount") === 0 &&
        subject.isInGroup("plugdev")) {
        return polkit.Result.YES;
    }
});

</code></pre>
</div>
<p>Finally, let&#8217;s create a <code>udiskie.service</code> file to ensure udiskie is working all the time. Let&#8217;s use <code>sudo nano /etc/systemd/system/udiskie.service</code>:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>[Unit]
Description=Auto-mount USB drives with udiskie
After=graphical.target network-online.target

[Service]
User=pi
ExecStart=/usr/bin/udiskie --automount
Restart=on-failure

[Install]
WantedBy=default.target
</code></pre>
</div>
<p>Then let&#8217;s restart all services that are used:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>sudo systemctl restart polkit
sudo systemctl restart udisks2
sudo systemctl restart udiskie
</code></pre>
</div>
<p>And we&#8217;re done with <code>udiskie</code>!</p>
<h2>
<p>  Modifying PicFrame Configuration<br />
</h2>
<p>Configuration change to PicFrame is simple. We&#8217;re going to change the photo directory to <code>/media</code>, the location where drives are mounted (e.g. <code>/media/pi/E342-JS2J</code>)
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>nano picframe_data/config/configuration.yaml
</code></pre>
</div>
<p>And inside model, let&#8217;s change this line:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>pic_dir: "/media" # default="/home/pi/Pictures", root folder for images

</code></pre>
</div>
<p>And we are done with PicFrame Configuration! With this we should be done! Reboot the system and insert a flash drive or two to see if pictures from all drives are coming to PicFrame.</p>
</div>
<p>Expand your digital picture frame use cases with Pi3D integrated USB media stick support using UDiskie &#8211; <a href="https://dev.to/leocolman/expand-your-digital-picture-frame-use-cases-with-pi3d-integrated-usb-media-stick-support-using-2737">Original at https://dev.to/leocolman/expand-your-digital-picture-frame-use-cases-with-pi3d-integrated-usb-media-stick-support-using-2737</a> by Leonardo Colman Lopes </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Converting an old TV Box into an epic Picture Frame</title>
		<link>https://dev.to/leocolman/converting-an-old-tv-box-into-an-epic-picture-frame-3iob</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Fri, 07 Mar 2025 16:18:15 +0000</pubDate>
				<category><![CDATA[Sem categoria]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2025/03/07/converting-an-old-tv-box-into-an-epic-picture-frame-3iob/</guid>

					<description><![CDATA[I built and customized 3 instances of The Digital Picture Frame. It&#8217;s so simple yet such a great system setup that I fell in love. People have built such awesome setups for this small utility, and passion drove me to my calling: It&#8217;s my time to hack a device and turn it into a Picture <a class="read-more" href="https://dev.to/leocolman/converting-an-old-tv-box-into-an-epic-picture-frame-3iob">Read more</a>]]></description>
										<content:encoded><![CDATA[<div>
<p>I built and customized 3 instances of <a href="https://www.thedigitalpictureframe.com/how-to-build-the-best-raspberry-pi-digital-picture-frame-with-bookworm-wayland-2025-edition-pi-2-3-4-5/" rel="noopener noreferrer">The Digital Picture Frame</a>. It&#8217;s so simple yet such a great system setup that I fell in love.</p>
<p>People have built such <a href="https://www.thedigitalpictureframe.com/the-digital-picture-frame-user-gallery-with-great-examples-and-inspiration-for-your-raspberry-pi-projects/" rel="noopener noreferrer">awesome setups</a> for this small utility, and passion drove me to my calling: It&#8217;s my time to hack a device and turn it into a Picture Frame.</p>
<p>My chosen target? An old TV Box:<br />
<a href="https://leonardo.colman.com.br/wp-content/uploads/2025/03/99ebhx0kozy9k2zc2lz8.png" class="article-body-image-wrapper"><img decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/03/99ebhx0kozy9k2zc2lz8.png" alt="TV Box" width="800" height="800"></a></p>
<p>It has everything needed:</p>
<ul>
<li>A Hackable Android TV that I can turn into Linux</li>
<li>A way to boot a custom system (i.e. an SD card)</li>
<li>A way to mount different media for photo display (i.e. Flash Drives)</li>
</ul>
<p>Let&#8217;s dive in this journey and perhaps you will have a TV Box-Frame too!</p>
<p><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>A Word of Warning First</strong>:<br />
Not all TV Boxes will work. Some (if not most) are not able to host a custom system. Be wary when following this as a guide as your setup might vary.</p>
<h2>
<p>  Installing Armbian<br />
</h2>
<p><a href="https://github.com/armbian" rel="noopener noreferrer">Armbian</a> is a &#8220;Minimal custom Debian or Ubuntu based Linux perfect for server and IOT&#8221;. We&#8217;re going to pick it as most TV Boxes are ARM-based, and Armbian is perfect for the job.</p>
<p>We are going to follow the guide <a href="https://forum.armbian.com/topic/34923-csc-armbian-for-rk322x-tv-box-boards/" rel="noopener noreferrer">CSC Armbian for RK322x TV box boards</a>.</p>
<p><a href="https://leonardo.colman.com.br/wp-content/uploads/2025/03/xk1wpvkmlfpq2voervxf.png" class="article-body-image-wrapper"><img decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/03/xk1wpvkmlfpq2voervxf.png" alt="Armbian" width="481" height="352"></a></p>
<p><strong>For this tutorial we&#8217;re going to use a <code>RK3229</code> as our pilot chip</strong>. It&#8217;s available in many TV Boxes and is not hard to setup</p>
<h3>
<p>  Discovering the Chipset<br />
</h3>
<p>If you&#8217;re not sure that your TV Box chip is supported, download and install through the TV Box the app <code>Z-CPU</code> or the FLOSS App <a href="https://f-droid.org/en/packages/com.kgurgul.cpuinfo/" rel="noopener noreferrer"><code>CPU Info</code></a>.</p>
<p>In the app we&#8217;re looking for information on the chipset. <strong>Chipsets of the format <code>RK322X</code> are supported.</strong> </p>
<h3>
<p>  Unbricking Instructions<br />
</h3>
<p>Technically, rockchip devices <strong>cannot be bricked</strong>. In some cases a screw-up might require repairing of the original system, and the unbricking instructions from <a href="https://forum.armbian.com/topic/34923-csc-armbian-for-rk322x-tv-box-boards/" rel="noopener noreferrer">CSC Armbian for RK322x TV box boards</a> can help.</p>
<h3>
<p>  Installing Using SD Card<br />
</h3>
<p>We&#8217;re going to use an SDXC Card (Or most commonly an SD Card + Adapter). This card will hold our entire operating system*, and will be plugged in the machine 24/7. I recommend anything 8GB or bigger, but realistically 3GB is the entire system + files.</p>
<p>* You could instead install the system directly to the machine. I couldn&#8217;t get that to work reliably and preferred the permanent SD approach.</p>
<h3>
<p>  Preparing the TV Box with Multitool<br />
</h3>
<p>The Multitool is a small but powerful tool to make changes to our RK322X.</p>
<ul>
<li><a href="https://users.armbian.com/jock/web/rk322x/multitool/multitool.img.xz" rel="noopener noreferrer">Multitool &#8211; A small but powerful image for RK322x TV Box maintenance<br />
</a></li>
</ul>
<p>Let&#8217;s flash the Multitool image onto the SD Card first. You can use any tool for it, but I like <a href="https://etcher.balena.io/" rel="noopener noreferrer">Balena Etcher</a>.</p>
<p><a href="https://leonardo.colman.com.br/wp-content/uploads/2025/03/wy14ptx5a9e41eeot7bv.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/03/wy14ptx5a9e41eeot7bv.png" alt="Balena Etcher" width="798" height="511"></a></p>
<p>Our goal with Multitool is straightforward:</p>
<ol>
<li>Flash Multitool to the SD Card</li>
<li>Put the card in the TV Box and power on. After a few seconds Multitool should appear on the display</li>
<li>
<strong><em>RECOMMENDED</em></strong> Make a backup of the current firmware with <strong>Backup Flash</strong>
</li>
<li>Select <strong>Install Jump Start for Armbian</strong>
</li>
</ol>
<p><a href="https://leonardo.colman.com.br/wp-content/uploads/2025/03/aipj6gdi0evz9pf8uur2.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/03/aipj6gdi0evz9pf8uur2.png" alt="Multitool" width="792" height="556"></a></p>
<p>And we&#8217;re done with Multitool! Power the device down and retrieve the SD card. Store your backup and get ready to flash it again!</p>
<h3>
<p>  Choosing an Image<br />
</h3>
<p><a href="https://forum.armbian.com/topic/34923-csc-armbian-for-rk322x-tv-box-boards/" rel="noopener noreferrer">CSC Armbian for RK322x TV box boards</a> explains how to make a build, but we are going to grab a pre-built binary from <a href="https://github.com/armbian/community/releases/latest" rel="noopener noreferrer">armbian/community</a>.</p>
<p><a href="https://leonardo.colman.com.br/wp-content/uploads/2025/03/ymrpfewywydz3emretfx.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/03/ymrpfewywydz3emretfx.png" alt="Armbian" width="800" height="123"></a><br />
Let&#8217;s take the image <code>Rk322x-box_bookworm_current_6.12.15_minimal.img.xz</code></p>
<p>As we&#8217;ve done earlier, let&#8217;s flash it to our SD card using Balena Etcher.<br />
After flashing, insert the SD with the new Armbian image on the device and power it on.</p>
<h3>
<p>  Welcome to Armbian<br />
</h3>
<p>If everything worked correctly, you should now have a shell inside Armbian asking you for information.<br />
The most important point here is that we&#8217;ll create a normal user account with the username <code>pi</code>. This will be important to follow <a href="https://www.thedigitalpictureframe.com/how-to-build-the-best-raspberry-pi-digital-picture-frame-with-bookworm-wayland-2025-edition-pi-2-3-4-5/" rel="noopener noreferrer">The Digital Picture Frame</a> without too many changes on the scripts.</p>
<p><a href="https://leonardo.colman.com.br/wp-content/uploads/2025/03/gvdfpgvp8b3wy591dv9t.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/03/gvdfpgvp8b3wy591dv9t.png" alt="Armbian" width="800" height="384"></a></p>
<p>To finish configuring Armbian, let&#8217;s run two commands:</p>
<ul>
<li>
<code>sudo rk322x-config</code> and select your board characteristics to enable leds, wifi chips, etc.</li>
<li>
<code>armbian-config</code> to configure timezone, locales and other personal options</li>
</ul>
<p>At this point you can also connect your device to your network and connect to it through <code>ssh</code>.</p>
<h3>
<p>  Install The Digital PictureFrame<br />
</h3>
<p>From this point on we are done with the system and can start playing with PictureFrame itself. We are going to follow <a href="https://www.thedigitalpictureframe.com/how-to-build-the-best-raspberry-pi-digital-picture-frame-with-bookworm-wayland-2025-edition-pi-2-3-4-5/" rel="noopener noreferrer">the guide</a> for Raspberries as close as we can until we get it working.</p>
<p>Let&#8217;s first install necessary dependencies:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>sudo apt update
sudo apt install build-essential pkg-config python3-dev python3-pip python3.11-venv libjpeg-dev libtiff-dev libfreetype6-dev zlib1g-dev liblcms2-dev libwebp-dev tcl-dev tk-dev libharfbuzz-dev libfribidi-dev libxcb1-dev libpng-dev cmake ninja-build git libgles2-mesa libegl1-mesa libgl1-mesa-dri libglapi-mesa libsdl2-2.0-0 libsdl2-dev

</code></pre>
</div>
<p>And compile + install HEIF, as the ones from Armbian aren&#8217;t updated enough:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>cd ~
git clone https://github.com/strukturag/libheif.git
cd libheif

mkdir build && cd build
cmake ..
make -j$(nproc)
sudo make install

</code></pre>
</div>
<p>Then let&#8217;s prepare a Python VEnv to install picframe into:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>mkdir venv_picframe

python -m venv /home/pi/venv_picframe

source venv_picframe/bin/activate
</code></pre>
</div>
<p>And finally, let&#8217;s install PicFrame!
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>pip install picframe
</code></pre>
</div>
<p>To know it worked, we should see after a long (maybe 30+ minutes) some positive messages:</p>
<blockquote>
<p>Successfully installed IPTCInfo3-2.1.4 Pillow-11.1.0 PyYAML-6.0.2 defusedxml-0.7.1 ninepatch-0.2.0 numpy-2.2.3 paho-mqtt-2.1.0 pi-heif-0.21.0 pi3d-2.53 picframe-2024.11.1 pysdl2-0.9.17</p>
</blockquote>
<h3>
<p>  Configuring PicFrame<br />
</h3>
<p>On <a href="https://www.thedigitalpictureframe.com/how-to-build-the-best-raspberry-pi-digital-picture-frame-with-bookworm-wayland-2025-edition-pi-2-3-4-5/#Configuring_PictureFrame" rel="noopener noreferrer">Configuring PicFrame</a> we have clear instructions on how to configure picframe. Let&#8217;s do that now!</p>
<p>You will be asked three questions for the basic configuration settings. Just hit Enter to keep the default for now. You can always change these settings later.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>mkdir {Pictures,DeletedPictures}

picframe -i /home/pi/
</code></pre>
</div>
<p>You don’t need to make changes in the configuration file at this point. But you probably want to customize it later. In that case, open it with
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>nano ~/picframe_data/config/configuration.yaml
</code></pre>
</div>
<h3>
<p>  Starting Picframe<br />
</h3>
<p>Let&#8217;s create a script to start our Picframe:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>nano start_picframe.sh

#!/bin/bash
source /home/pi/venv_picframe/bin/activate # activate phyton virtual env
picframe &  #start picframe

while true; do sleep 10; done # Keep the script running
</code></pre>
</div>
<p>Save and close and make the file executable with
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>chmod +x ./start_picframe.sh
</code></pre>
</div>
<p>And now, finally, let&#8217;s run our PicFrame
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>./start_picframe.sh
</code></pre>
</div>
<p>And if everything went through correctly, we&#8217;ll get a beautiful mug on the screen:</p>
<p><a href="https://leonardo.colman.com.br/wp-content/uploads/2025/03/eyz62o7li2q4vemn795i.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/03/eyz62o7li2q4vemn795i.png" alt="Image description" width="800" height="428"></a></p>
<h3>
<p>  Auto Starting Picframe<br />
</h3>
<p>This part will work a bit different from <a href="https://www.thedigitalpictureframe.com/how-to-build-the-best-raspberry-pi-digital-picture-frame-with-bookworm-wayland-2025-edition-pi-2-3-4-5/#Autostarting_PictureFrame" rel="noopener noreferrer">Autostarting PictureFrame</a>, so pay close attention!</p>
<p>Let&#8217;s start by creating the file <code>picframe.service</code> with <code>sudo nano /etc/systemd/system/picframe.service</code> and pasting the following content inside:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>[Unit]
Description=PictureFrame Startup
After=multi-user.target

[Service]
Type=simple
ExecStart=/bin/bash /home/pi/start_picframe.sh
Restart=on-failure  # Only restart if it fails
RestartSec=10       # Wait 10 seconds before restarting
User=pi
WorkingDirectory=/home/pi
Environment=DISPLAY=:0
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
</code></pre>
</div>
<p>Then reload systemd and enable the brand new service:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>sudo systemctl daemon-reload
sudo systemctl enable picframe.service
sudo systemctl start picframe.service
</code></pre>
</div>
<p>We can check the service status by running
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>systemctl status picframe.service
</code></pre>
</div>
<h3>
<p>  Enable Autologin on tty1<br />
</h3>
<p>The <code>pi</code> user doesn&#8217;t have auto-login enabled, and we must have it if we don&#8217;t want to stare at a login screen. Let&#8217;s change that by creating an override for <code>getty@tty1</code>:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>sudo mkdir -p /etc/systemd/system/getty@tty1.service.d
sudo nano /etc/systemd/system/getty@tty1.service.d/override.conf

</code></pre>
</div>
<p>And paste this inside to enable auto-login:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin pi --noclear %I $TERM
</code></pre>
</div>
<p>PictureFrame should now start on boot!</p>
</div>
<p>Converting an old TV Box into an epic Picture Frame &#8211; <a href="https://dev.to/leocolman/converting-an-old-tv-box-into-an-epic-picture-frame-3iob">Original at https://dev.to/leocolman/converting-an-old-tv-box-into-an-epic-picture-frame-3iob</a> by Leonardo Colman Lopes </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Automated Screenshot Taking in Android using Kotest and Ktor</title>
		<link>https://dev.to/leocolman/automated-screenshot-taking-in-android-using-kotest-and-ktor-o71</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Sun, 26 Jan 2025 12:43:19 +0000</pubDate>
				<category><![CDATA[android]]></category>
		<category><![CDATA[kotest]]></category>
		<category><![CDATA[kotlin]]></category>
		<category><![CDATA[ktor]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2025/01/26/automated-screenshot-taking-in-android-using-kotest-and-ktor-o71/</guid>

					<description><![CDATA[1. Introduction Automated screenshot testing is essential in Android development for ensuring UI consistency across updates and devices. Manually verifying UI changes is time-consuming and error-prone, making automation a vital practice. In this guide, we’ll show how to use Kotest for capturing screenshots and Ktor for uploading them to a local server. This setup streamlines <a class="read-more" href="https://dev.to/leocolman/automated-screenshot-taking-in-android-using-kotest-and-ktor-o71">Read more</a>]]></description>
										<content:encoded><![CDATA[<div>
<h2>
<p>  1. Introduction<br />
</h2>
<p>Automated screenshot testing is essential in Android development for ensuring UI consistency across updates and devices. Manually verifying UI changes is time-consuming and error-prone, making automation a vital practice.</p>
<p>In this guide, we’ll show how to use Kotest for capturing screenshots and Ktor for uploading them to a local server. This setup streamlines UI validation, improves efficiency, and centralizes screenshot storage for better collaboration and quality assurance.</p>
<h2>
<p>  2. Setup<br />
</h2>
<p>To get started with automated screenshot testing using Kotest and Ktor, we’ll need to prepare our project by configuring the necessary tools and libraries.</p>
<p>Update our project’s <em>build.gradle.kts</em> file to include the required libraries for testing and screenshot uploads. Below are the key dependencies:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>dependencies {
    // Kotest for writing tests
    androidTestImplementation("br.com.colman:kotest-runner-android:${version}")

    // Fuel library for uploading screenshots
    androidTestImplementation("com.github.kittinunf.fuel:fuel:${version}")
}

</code></pre>
</div>
<p>Here:</p>
<ul>
<li>
<a href="https://kotest.io/" rel="noopener noreferrer">Kotest</a> provides the framework for writing and running tests.</li>
<li>
<a href="https://github.com/LeoColman/kotest-android/tree/main" rel="noopener noreferrer">Kotest-Android</a> provides specific bindings for running Android tests.</li>
<li>
<a href="https://github.com/kittinunf/fuel" rel="noopener noreferrer">Fuel</a> simplifies HTTP requests for uploading screenshots during tests.</li>
</ul>
<h2>
<p>  3. Preparing a Local Server to Receive Screenshots<br />
</h2>
<p>A critical component of automated screenshot testing is a server to store the captured images for validation or archival purposes. We’ll use Ktor, a lightweight Kotlin-based framework, to set up a local server capable of handling screenshot uploads.</p>
<p>Create a Kotlin script file named <em>refresh_screenshots.main.kts</em>. This script will configure and launch a local server using Ktor.</p>
<h3>
<p>  3.1. Disabling Animations<br />
</h3>
<p>Animations in the Android emulator can affect screenshot consistency. The script disables them using ADB commands:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>fun disableAnimations() {
    listOf(
        "adb shell settings put global window_animation_scale 0",
        "adb shell settings put global transition_animation_scale 0",
        "adb shell settings put global animator_duration_scale 0"
    ).forEach { command ->
        try {
            val process = Runtime.getRuntime().exec(command)
            process.waitFor()
        } catch (e: Exception) {
            println("Error while trying to disable animations via ADB: ${e.localizedMessage}")
        }
    }
    println("Animations in the emulator have been disabled")
}
</code></pre>
</div>
<h3>
<p>  3.2. Starting the Ktor Server<br />
</h3>
<p>The server listens on port 8081 and handles file uploads. Uploaded screenshots are stored in language-specific directories based on parameters (lang and country) in the request:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>fun startServer(): NettyApplicationEngine {
    return embeddedServer(Netty, port = 8081) {
        install(ContentNegotiation) {
            json()
        }
        routing {
            post("/upload") {
                val multipart = call.receiveMultipart()
                val lang = call.parameters["lang"] ?: "unknown"
                val country = call.parameters["country"] ?: "unknown"

                multipart.forEachPart { part ->
                    if (part is PartData.FileItem) {
                        val countryHyphen = if (country.isNotBlank()) "-$country" else ""
                        val dir = File("../fastlane/metadata/android/${lang}${countryHyphen}/images/phoneScreenshots")
                        if (!dir.exists()) {
                            dir.mkdirs()
                        }
                        val file = File(dir, part.originalFileName ?: "screenshot.png")
                        part.streamProvider().use { input -> file.outputStream().buffered().use { input.copyTo(it) } }
                    }
                    part.dispose()
                }
                call.respondText("File uploaded successfully", status = HttpStatusCode.OK)
            }
        }
    }.start(wait = false)
}
</code></pre>
</div>
<h3>
<p>  3.3. Running Tests and Handling the Server<br />
</h3>
<p>The script runs Android tests using Gradle and stops the server once the tests are completed:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>val server = startServer()

disableAnimations()

// Run Android tests
val process = ProcessBuilder(
    "../gradlew",
    "connectedFdroidDebugAndroidTest",
    "-Pandroid.testInstrumentationRunnerArguments.class=br.com.colman.petals.ScreenshotTakerTest"
).inheritIO().start()
val exitCode = process.waitFor()

server.stop(1000, 10000)

</code></pre>
</div>
<h3>
<p>  3.4. Full Script<br />
</h3>
<p>Let&#8217;s take a look at the final result by merging all the scripts and adding relevant imports:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>#!/usr/bin/env kotlin

@file:DependsOn("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.1")
@file:DependsOn("io.ktor:ktor-server-core-jvm:2.3.13")
@file:DependsOn("io.ktor:ktor-server-netty-jvm:2.3.13")
@file:DependsOn("io.ktor:ktor-server-content-negotiation-jvm:2.3.13")
@file:DependsOn("io.ktor:ktor-serialization-kotlinx-json-jvm:2.3.13")
@file:DependsOn("io.ktor:ktor-server-host-common-jvm:2.3.13")

import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.File

// Function to disable animations via ADB
fun disableAnimations() {
  listOf(
    "adb shell settings put global window_animation_scale 0",
    "adb shell settings put global transition_animation_scale 0",
    "adb shell settings put global animator_duration_scale 0"
  ).forEach { command ->
    try {
      val process = Runtime.getRuntime().exec(command)
      process.waitFor()
    } catch (e: Exception) {
      println("Error while trying to disable animations via ADB: ${e.localizedMessage}")
    }
  }
  println("Animations in the emulator have been disabled")
}

fun startServer(): NettyApplicationEngine {
  return embeddedServer(Netty, port = 8081) {
    install(ContentNegotiation) {
      json()
    }
    routing {
      post("/upload") {
        val multipart = call.receiveMultipart()
        val lang = call.parameters["lang"] ?: "unknown"
        val country = call.parameters["country"] ?: "unknown"

        multipart.forEachPart { part ->
          if (part is PartData.FileItem) {
            val countryHyphen = if (country.isNotBlank()) "-$country" else ""
            val dir = File("../fastlane/metadata/android/${lang}${countryHyphen}/images/phoneScreenshots")
            if (!dir.exists()) {
              dir.mkdirs()
            }
            val file = File(dir, part.originalFileName ?: "screenshot.png")
            part.streamProvider().use { input -> file.outputStream().buffered().use { input.copyTo(it) } }
          }
          part.dispose()
        }
        call.respondText("File uploaded successfully", status = HttpStatusCode.OK)
      }
    }
  }.start(wait = false)
}


// Start the server
val server = startServer()

// Disable animations
disableAnimations()

// Run Android tests
val process = ProcessBuilder(
  "../gradlew",
  "connectedFdroidDebugAndroidTest",
  "-Pandroid.testInstrumentationRunnerArguments.class=br.com.colman.petals.ScreenshotTakerTest"
).inheritIO().start()
val exitCode = process.waitFor()

// Stop the server
server.stop(1000, 10000)

</code></pre>
</div>
<h2>
<p>  4. Writing a Screenshot Test in Kotest<br />
</h2>
<p>This section demonstrates how to create comprehensive screenshot tests using Kotest in an Android project. The provided code integrates Jetpack Compose testing with screenshot capture and upload functionality, supporting multiple locales and organized storage on a local server.</p>
<h3>
<p>  4.1. Key Features of the Screenshot Tests<br />
</h3>
<p><strong>1. Multi-Locale Testing</strong><br />
The tests iterate over a list of language (lang) and country (country) codes, ensuring the UI is tested across different locales.</p>
<p><strong>2. Screenshot Capture</strong><br />
Screenshots are taken directly from the UI during tests using Jetpack Compose&#8217;s testing utilities.</p>
<p><strong>3. Automated Uploads</strong><br />
Screenshots are uploaded to a local server via HTTP, organized into directories by language and country.</p>
<h3>
<p>  4.2. Multi-Locale Screenshot Tests<br />
</h3>
<p>Below is the main test class, which runs multiple tests and captures screenshots at different points:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>class ScreenshotTakerTest : FunSpec({

    val locales = listOf(
        "de" to "DE",
        "en" to "US",
        "es" to "ES",
        "fr" to "FR",
        "it" to "IT",
        "pt" to "BR",
        "ru" to "RU",
        "tr" to "TR",
        "uk" to ""
    )

    test("Screenshot for Main Page") {
        runAndroidComposeUiTest<MainActivity> {
            locales.forEach { (lang, country) ->
                activity?.setLocale(Locale(lang, country))
                waitForIdle()

                // Interact with UI
                onNodeWithTag("UsageMainColumn").performTouchInput { swipeUp() }
                waitForIdle()

                // Capture and upload screenshot
                takeScreenshot("main_page.png", lang, country)
            }
        }
    }
})

</code></pre>
</div>
<h3>
<p>  4.3. Helper Functions<br />
</h3>
<p>The implementation relies on utility functions for common operations.</p>
<p>Setting the App Locale Updates the app’s locale dynamically during tests:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>private fun MainActivity.setLocale(locale: Locale) {
    val resources = baseContext.resources
    Locale.setDefault(locale)
    val config = resources.configuration
    config.setLocale(locale)
    resources.updateConfiguration(config, resources.displayMetrics)
    runOnUiThread { recreate() }
}
</code></pre>
</div>
<p>Capturing a Screenshot Uses Jetpack Compose&#8217;s <em>captureToImage()</em> to capture the current UI state:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>private fun AndroidComposeUiTest<*>.takeScreenshot(file: String, lang: String, country: String) {
    sleep(3000) // Wait for animations to settle
    val bitmap = onRoot().captureToImage().asAndroidBitmap()
    uploadScreenshot(bitmap, file, lang, country)
}
</code></pre>
</div>
<p>Uploading the Screenshot Sends the captured screenshot to the local server:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>private fun uploadScreenshot(bitmap: Bitmap, fileName: String, lang: String, country: String) {
    val tempFile = File.createTempFile(fileName, null).apply {
        outputStream().use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
    }
    Fuel.upload("http://10.0.2.2:8081/upload?lang=$lang&country=$country")
        .add(FileDataPart(tempFile, name = "file", filename = fileName))
        .response()
}
</code></pre>
</div>
<p>Note: <em>10.0.2.2</em> is the ip address of the computer running the emulator. It&#8217;s an <a href="https://developer.android.com/studio/run/emulator-networking" rel="noopener noreferrer">special address</a></p>
<h2>
<p>  5. Conclusion<br />
</h2>
<p>Automating screenshot testing in Android using Kotest and Ktor streamlines the UI validation process and ensures consistency across multiple locales. By combining Jetpack Compose testing utilities with an efficient local server, developers can easily capture, organize, and analyze screenshots during automated tests.</p>
<p>This approach offers several benefits:</p>
<ul>
<li>Improved Efficiency: Automates tedious manual testing tasks.</li>
<li>Comprehensive Coverage: Validates UI behavior across different languages, countries, and configurations.</li>
<li>Centralized Storage: Screenshots are uploaded to a server and organized systematically for future analysis.</li>
</ul>
<p>By adopting this solution, teams can reduce testing overhead, improve collaboration, and ensure a polished, reliable user interface. This setup can further be extended with CI/CD pipelines, enabling seamless integration into modern development workflows.</p>
<p>Start implementing automated screenshot testing today to enhance your app’s quality and streamline your development process.</p>
</div>
<p>Automated Screenshot Taking in Android using Kotest and Ktor &#8211; <a href="https://dev.to/leocolman/automated-screenshot-taking-in-android-using-kotest-and-ktor-o71">Original at https://dev.to/leocolman/automated-screenshot-taking-in-android-using-kotest-and-ktor-o71</a> by Leonardo Colman Lopes </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Check if Mono is Empty with Kotlin</title>
		<link>https://www.baeldung.com/kotlin/mono-empty-check</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Thu, 23 Jan 2025 19:16:10 +0000</pubDate>
				<category><![CDATA[Sem categoria]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2025/01/23/mono-empty-check/</guid>

					<description><![CDATA[Learn to check if a Mono is empty in Kotlin and define static and dynamic fallback values to create robust reactive flows. Check if Mono is Empty with Kotlin &#8211; Original at https://www.baeldung.com/kotlin/mono-empty-check by @baeldung]]></description>
										<content:encoded><![CDATA[<div>
<div><img decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/01/Kotlin-Featured-Image-04.jpg" style="width: 100%;"></p>
<div>Learn to check if a Mono is empty in Kotlin and define static and dynamic fallback values to create robust reactive flows.</div>
</div>
</div>
<p>Check if Mono is Empty with Kotlin &#8211; <a href="https://www.baeldung.com/kotlin/mono-empty-check">Original at https://www.baeldung.com/kotlin/mono-empty-check</a> by @baeldung </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Excluding a Library from All Dependencies in Kotlin DSL in Gradle</title>
		<link>https://www.baeldung.com/kotlin/gradle-exclude-library-all-dependencies</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Sun, 29 Dec 2024 09:35:58 +0000</pubDate>
				<category><![CDATA[Sem categoria]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2024/12/29/gradle-exclude-library-all-dependencies/</guid>

					<description><![CDATA[Learn how to exclude a library or a transitive dependency using the Kotlin DSL syntax in a Gradle. Excluding a Library from All Dependencies in Kotlin DSL in Gradle &#8211; Original at https://www.baeldung.com/kotlin/gradle-exclude-library-all-dependencies by @baeldung]]></description>
										<content:encoded><![CDATA[<div>
<div><img decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/01/Kotlin-Featured-Image-06.jpg" style="width: 100%;"></p>
<div>Learn how to exclude a library or a transitive dependency using the Kotlin DSL syntax in a Gradle.</div>
</div>
</div>
<p>Excluding a Library from All Dependencies in Kotlin DSL in Gradle &#8211; <a href="https://www.baeldung.com/kotlin/gradle-exclude-library-all-dependencies">Original at https://www.baeldung.com/kotlin/gradle-exclude-library-all-dependencies</a> by @baeldung </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Building a Kotlin Library Using Gradle</title>
		<link>https://www.baeldung.com/kotlin/gradle-library</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Sun, 17 Nov 2024 16:47:26 +0000</pubDate>
				<category><![CDATA[Sem categoria]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2024/11/17/gradle-library/</guid>

					<description><![CDATA[A quick tutorial on building a library in Kotlin with Gradle. Building a Kotlin Library Using Gradle &#8211; Original at https://www.baeldung.com/kotlin/gradle-library by @baeldung]]></description>
										<content:encoded><![CDATA[<div>
<div><img decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/01/Kotlin-Featured-Image-03-k-2.png" style="width: 100%;"></p>
<div>A quick tutorial on building a library in Kotlin with Gradle.</div>
</div>
</div>
<p>Building a Kotlin Library Using Gradle &#8211; <a href="https://www.baeldung.com/kotlin/gradle-library">Original at https://www.baeldung.com/kotlin/gradle-library</a> by @baeldung </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Dagger 2 Constructor Injection with Named Arguments in Kotlin</title>
		<link>https://www.baeldung.com/kotlin/dagger-named-constructor-injection</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Fri, 25 Oct 2024 23:16:45 +0000</pubDate>
				<category><![CDATA[Sem categoria]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2024/10/25/dagger-named-constructor-injection/</guid>

					<description><![CDATA[A quick tutorial on using named injection with Dagger 2 in Kotlin. Dagger 2 Constructor Injection with Named Arguments in Kotlin &#8211; Original at https://www.baeldung.com/kotlin/dagger-named-constructor-injection by @baeldung]]></description>
										<content:encoded><![CDATA[<div>
<div><img decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/01/Kotlin-Featured-Image.png" style="width: 100%;"></p>
<div>A quick tutorial on using named injection with Dagger 2 in Kotlin.</div>
</div>
</div>
<p>Dagger 2 Constructor Injection with Named Arguments in Kotlin &#8211; <a href="https://www.baeldung.com/kotlin/dagger-named-constructor-injection">Original at https://www.baeldung.com/kotlin/dagger-named-constructor-injection</a> by @baeldung </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migration from Dagger to Koin</title>
		<link>https://www.baeldung.com/kotlin/dagger-to-koin-migrate</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Sat, 12 Oct 2024 22:36:22 +0000</pubDate>
				<category><![CDATA[Sem categoria]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2024/10/12/dagger-to-koin-migrate/</guid>

					<description><![CDATA[A quick tutorial on migrating a Kotlin application from Dagger to Koin for dependency injection. Migration from Dagger to Koin &#8211; Original at https://www.baeldung.com/kotlin/dagger-to-koin-migrate by @baeldung]]></description>
										<content:encoded><![CDATA[<div>
<div><img decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/01/Kotlin-Featured-Image-03-k-1.png" style="width: 100%;"></p>
<div>A quick tutorial on migrating a Kotlin application from Dagger to Koin for dependency injection.</div>
</div>
</div>
<p>Migration from Dagger to Koin &#8211; <a href="https://www.baeldung.com/kotlin/dagger-to-koin-migrate">Original at https://www.baeldung.com/kotlin/dagger-to-koin-migrate</a> by @baeldung </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Implement Scheduler/Timer with Kotlin Coroutine</title>
		<link>https://www.baeldung.com/kotlin/coroutine-timer-scheduler</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Tue, 17 Sep 2024 05:43:55 +0000</pubDate>
				<category><![CDATA[Sem categoria]]></category>
		<guid isPermaLink="false">https://leonardo.colman.com.br/2024/09/17/coroutine-timer-scheduler/</guid>

					<description><![CDATA[A quick tutorial on creating timers and schedulers with Kotlin coroutines. Implement Scheduler/Timer with Kotlin Coroutine &#8211; Original at https://www.baeldung.com/kotlin/coroutine-timer-scheduler by @baeldung]]></description>
										<content:encoded><![CDATA[<div>
<div><img decoding="async" src="https://leonardo.colman.com.br/wp-content/uploads/2025/01/Kotlin-Featured-Image-03-k.png" style="width: 100%;"></p>
<div>A quick tutorial on creating timers and schedulers with Kotlin coroutines.</div>
</div>
</div>
<p>Implement Scheduler/Timer with Kotlin Coroutine &#8211; <a href="https://www.baeldung.com/kotlin/coroutine-timer-scheduler">Original at https://www.baeldung.com/kotlin/coroutine-timer-scheduler</a> by @baeldung </p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Migrating to Microservice Databases</title>
		<link>https://leonardo.colman.com.br/review/migrating-to-microservice-databases/</link>
		
		<dc:creator><![CDATA[Leonardo Colman Lopes]]></dc:creator>
		<pubDate>Fri, 17 Nov 2023 19:34:37 +0000</pubDate>
				<guid isPermaLink="false">https://leonardo.colman.com.br/?post_type=rcno_review&#038;p=379</guid>

					<description><![CDATA[Leitura curta e rápido que vale a pena para quem já entende um pouco do assunto]]></description>
										<content:encoded><![CDATA[
<div class="rcno-book-review-content"></div><!--- Recencio Book Reviews --->]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
