Jekyll2024-02-02T14:43:50+00:00https://zarah.dev/feed.xmlZarah DominguezAn Android Love AffairZarah DominguezExtended ADB: En Vogue š2024-02-02T00:00:00+00:002024-02-02T00:00:00+00:00https://zarah.dev/2024/02/02/adb-model<p>Last year, I wrote about <a href="https://zarah.dev/2023/09/21/adb-devices.html">an extended <code class="language-plaintext highlighter-rouge">adb</code></a> script. The idea of the script is to make it really easy to issue an <code class="language-plaintext highlighter-rouge">adb</code> command even if there are multiple devices attached by presenting a chooser. For example, if I have two physical devices and an emulator and I want to use my deeplink <code class="language-plaintext highlighter-rouge">alias</code>, I get presented with a device chooser:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ deeplink https://zarah.dev
Multiple devices found:
1 - R5CR7039LBJ
2 - 39030FDJH01460
3 - emulator-5554
Select device:
</code></pre></div></div>
<p>I wrote about this alias and how it works <a href="https://zarah.dev/2023/08/30/adb-deeplinks.html">here</a>.</p>
<p>What I eventually learned is that I cannot remember which of those devices is my test phone (which has the app that handles the deeplink) and which is my personal phone. š¤¦āāļø It would be great if it also shows at least what <em>kind</em> of phone it is. Well, it turns out that <code class="language-plaintext highlighter-rouge">adb devices</code> <em>can</em> tell us this information! Hooray! The trick is to <a href="https://developer.android.com/tools/adb#devicestatus">include the <code class="language-plaintext highlighter-rouge">-l</code> option</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ adb devices <span class="nt">-l</span>
List of devices attached
R5CR7039LBJ device usb:35926016X product:p3sxxx model:SM_G998B device:p3s transport_id:1
39030FDJH01460 device usb:34930688X product:shiba model:Pixel_8 device:shiba transport_id:1
emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64 device:emu64a transport_id:2
</code></pre></div></div>
<p>As before, letās find all valid devices, dropping any unauthorised ones, but this time letās grab all the information up to the model name:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">valid_devices</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$all_devices</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"unauthorized"</span> | <span class="nb">grep</span> <span class="nt">-oE</span> <span class="s2">".*?model:</span><span class="se">\S</span><span class="s2">*"</span><span class="si">)</span>
</code></pre></div></div>
<p>At this point, the variable <code class="language-plaintext highlighter-rouge">valid_devices</code> contains the following:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R5CR7039LBJ device usb:35926016X product:p3sxxx model:SM_G998B
39030FDJH01460 device usb:34930688X product:shiba model:Pixel_8
emulator-5554 device product:sdk_gphone64_arm64 model:sdk_gphone64_arm64
</code></pre></div></div>
<p>The only other update our existing script needs is to include the model name when the list of devices is displayed.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">find_matches</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$valid_devices</span><span class="s2">"</span> | <span class="nb">awk</span> <span class="s1">'match($0, /model:/) {print NR " - " $1 " (" substr($0, RSTART+6) ")"}'</span><span class="si">)</span>
</code></pre></div></div>
<p>At the heart of it, what we need to do is extract pieces of information from each line; so <code class="language-plaintext highlighter-rouge">awk</code> should be good enough for this. When <code class="language-plaintext highlighter-rouge">awk</code> is invoked, it:</p>
<ul>
<li>reads the input line by line</li>
<li>stores each line in a variable <code class="language-plaintext highlighter-rouge">$0</code></li>
<li>splits each line into words</li>
<li>stores each word in variable from <code class="language-plaintext highlighter-rouge">$1..$n</code></li>
</ul>
<p>Thereās a lot of things happening in that <code class="language-plaintext highlighter-rouge">awk</code> command, so letās step through what it will do for each line in <code class="language-plaintext highlighter-rouge">valid_devices</code>:</p>
<table>
<tbody>
<tr>
<td colspan="4"><code>match($0, /model:/)</code></td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>match</code></td>
<td>built-in function that finds the first match of the provided regular expression</td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>$0</code></td>
<td>field variable containing the whole line</td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>/model:/</code></td>
<td>the regular expression to match ("model:"), <code>awk</code> syntax needs it to be inside slashes</td>
</tr>
<tr>
<td colspan="4"><code>print NR " - " $1 " (" substr($0, RSTART+6) ")"</code></td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>print</code></td>
<td>prints the succeeding items concatenated with the designated separator (default is a space)</td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>NR</code></td>
<td>the record number (i.e. line number being read)</td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>" - "</code></td>
<td>print a literal space, a dash, and a space</td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>$1</code></td>
<td>field variable containing the first word</td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>" ("</code></td>
<td>print a literal space and an open brace</td>
</tr>
<tr>
<td> </td>
<td colspan="2"><code>substr($0, RSTART+6)</code></td>
<td> </td>
</tr>
<tr>
<td> </td>
<td> </td>
<td><code>substr</code></td>
<td>built-in function to get a substring from <code>$0</code>, starting at index <code>RSTART+6</code></td>
</tr>
<tr>
<td> </td>
<td> </td>
<td><code>$0</code></td>
<td>field variable that contains the whole line</td>
</tr>
<tr>
<td> </td>
<td> </td>
<td><code>RSTART</code></td>
<td>the index of the last call to <code>match</code></td>
</tr>
<tr>
<td> </td>
<td> </td>
<td><code>+6</code></td>
<td>move the pointer six places (basically skip "model:")</code></td>
</tr>
<tr>
<td> </td>
<td><code>")"</code></td>
<td> </td>
<td> print a literal closing brace</td>
</tr>
</tbody>
</table>
<p>I found <a href="https://awk.js.org/help.html">awk.js.org</a> and <a href="https://www.jdoodle.com/execute-awk-online">jdoodle.com</a> really helpful when playing around with <code class="language-plaintext highlighter-rouge">awk</code>. I found the explanations in <code class="language-plaintext highlighter-rouge">awk.js.org</code> particularly useful.</p>
<p>Running the <code class="language-plaintext highlighter-rouge">deeplink</code> alias again now shows the model name inside braces:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ deeplink https://zarah.dev
Multiple devices found:
1 - R5CR7039LBJ <span class="o">(</span>SM_G998B<span class="o">)</span>
2 - 39030FDJH01460 <span class="o">(</span>Pixel_8<span class="o">)</span>
3 - emulator-5554 <span class="o">(</span>sdk_gphone64_arm64<span class="o">)</span>
Select device:
</code></pre></div></div>
<p>Much better! š I just need to make sure I donāt have to use more Samsungs cause I can <em>never</em> keep track of which Galaxy/Note/etc is which <code class="language-plaintext highlighter-rouge">SM_</code>. š
</p>
<p>As always, the gist is in <a href="https://gist.github.com/zmdominguez/9a889f1c367e1a21203ce8527c81e612">Github</a>.</p>Zarah DominguezLast year, I wrote about an extended adb script. The idea of the script is to make it really easy to issue an adb command even if there are multiple devices attached by presenting a chooser. For example, if I have two physical devices and an emulator and I want to use my deeplink alias, I get presented with a device chooser: ā ~ deeplink https://zarah.dev Multiple devices found: 1 - R5CR7039LBJ 2 - 39030FDJH01460 3 - emulator-5554 Select device:Extending an Interactive ADB š2023-09-21T00:00:00+00:002023-09-21T00:00:00+00:00https://zarah.dev/2023/09/21/adb-devices<p>A few weeks ago, I <a href="https://zarah.dev/2023/08/30/adb-deeplinks.html">wrote about a script</a> for making <code class="language-plaintext highlighter-rouge">adb</code> a little bit more interactive. The script makes the process of running an <code class="language-plaintext highlighter-rouge">adb</code> command much smoother if there are multiple devices attached by presenting a chooser. For example, when sending a deeplink:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ deeplink https://zarah.dev
Multiple devices found:
1 - R5CR7039LBJ
2 - emulator-5554
3 - emulator-5556
Select device:
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">adb</code> command to be sent is embedded in the script. It works fine if we only need the convenience to run one command, but letās face it, in reality I use a bunch of different commands all the time. It does not make sense though to have multiple copies of the script just to support multiple <code class="language-plaintext highlighter-rouge">adb</code> commands.</p>
<p>I mentioned in that post that it would be nice to be able to make the script generic enough to support multiple commands, and Iāve given it some thought since then.</p>
<p>Before we dive into possible solutions, I did notice an issue with the current version of the script. This line figures out how many devices are available:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Find how many devices we have</span>
<span class="nv">num_matches</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$all_devices</span> | egrep <span class="nt">-o</span> <span class="s2">"([[:alnum:]-]+[[:space:]]+device</span><span class="nv">$)</span><span class="s2">"</span> | <span class="nb">wc</span> <span class="nt">-l</span><span class="si">)</span>
</code></pre></div></div>
<p>To recap, it counts how many lines have some text followed by the word ādevicesā. It works most of the time, however I noticed that if I plug in a device that has the USB authorisations revoked, that device appears as āunauthorizedā.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ adb devices
List of devices attached
R5CR7039LBJ unauthorized
emulator-5556 device
</code></pre></div></div>
<p>For this post, that line has been updated to remove any lines with āunauthorizedā devices:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Drop any unauthorised devices (i.e. USB debugging disabled or authorisations revoked)</span>
<span class="nv">valid_devices</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$all_devices</span> | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s2">"([[:alnum:]-]+[[:space:]]+unauthorized</span><span class="nv">$)</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-oE</span> <span class="s2">"([[:alnum:]-]+[[:space:]]+device</span><span class="nv">$)</span><span class="s2">"</span><span class="si">)</span>
</code></pre></div></div>
<p>Back to the problem at hand: all <code class="language-plaintext highlighter-rouge">adb</code> commands are <a href="https://developer.android.com/tools/adb#issuingcommands">structured</a> in a predicatable manner:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb <span class="nt">-s</span> <SERIAL_NUMBER> <span class="nb">command</span>
</code></pre></div></div>
<p>We can take advantage of this pattern to extend the scalability of our script.</p>
<h3 id="option-1-pass-a-command-in-as-an-argument-ļø">Option 1: Pass a command in as an argument š£ļø</h3>
<p>I first explored the option of passing in a stub of the <code class="language-plaintext highlighter-rouge">adb</code> command as an argument to the script. If we take the deeplink command for example:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb <span class="nt">-s</span> <SERIAL_NUMBER> shell am start <span class="nt">-W</span> <span class="nt">-a</span> android.intent.action.VIEW <span class="nt">-d</span> <span class="s2">"SOME_URL"</span>
</code></pre></div></div>
<p>it means passing in <code class="language-plaintext highlighter-rouge">shell am start -W -a android.intent.action.VIEW -d "SOME_URL"</code> into the script. With the command stub now a parameter, weād have to change our <code class="language-plaintext highlighter-rouge">alias</code> from this:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">deeplink</span><span class="o">=</span><span class="s1">'zsh /Users/zarah/scripts/deeplink.sh $1'</span>
</code></pre></div></div>
<p>to this:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">deeplink</span><span class="o">=</span><span class="s1">'zsh /Users/zarah/scripts/deeplink.sh "shell am start -W -a android.intent.action.VIEW -d \"$1\""'</span>
</code></pre></div></div>
<p>With this option, the script remains mostly the same except for the part where the command is actually sent. Instead of hard-coding the command, we will use the stub passed in:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">command </span>adb <span class="nt">-s</span> <span class="nv">$serial_number</span> <span class="nv">$COMMAND</span>
</code></pre></div></div>
<p>This works, but itās not the best. There may be instances when we need to run multiple <code class="language-plaintext highlighter-rouge">adb</code> commands one after the other. For example, when setting the screen orientation to portrait:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>rotatePortrait<span class="o">()</span> <span class="o">{</span>
adb shell settings put system accelerometer_rotation 0
adb shell settings put system user_rotation 0
<span class="o">}</span>
</code></pre></div></div>
<p>If we use this version of the script, it <em>will</em> work, but it will also ask multiple times for the serial number. Thatās not good because it is easy to mess it up if different devices were entered for each command.</p>
<h3 id="option-2-just-make-it-get-the-serial-number-">Option 2: Just make it get the serial number š±</h3>
<p>In this option, we cut back the functionality of the script to make it do one thing: get the serial number. A big chunk of the script remains the same, the only change reallly is to make the <code class="language-plaintext highlighter-rouge">get_devices</code> function skip sending the command and return the serial number chosen instead:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># If there are multiple, ask for which device to grab</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nv">$num_matches</span> <span class="nt">-gt</span> 1 <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span>get_from_multiple
<span class="c"># Otherwise just grab the serial number</span>
<span class="k">else
</span><span class="nv">serial_number</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$valid_devices</span> | <span class="nb">awk</span> <span class="s1">'{printf $1}'</span><span class="si">)</span>
<span class="k">fi
</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$serial_number</span><span class="s2">"</span>
</code></pre></div></div>
<p>This means that issuing the actual command is up to the caller, which may sound annoying and repetitive. Do not fret though, because we can hide all the annoyingness in functions that we can use in our aliases.</p>
<p>In the <code class="language-plaintext highlighter-rouge">.zshrc</code> file (or wherever your <code class="language-plaintext highlighter-rouge">alias</code>es live), we can reference our <code class="language-plaintext highlighter-rouge">get_devices</code> script:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">source</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">dirname</span> <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span><span class="si">)</span><span class="s2">/get_devices.sh"</span>
</code></pre></div></div>
<p>The syntax to grab the returned value (the serial number) is a bit difficult to remember, so wrapping it in a function is helpful:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Grabs a serial number from all _available_ devices</span>
<span class="c"># If there is only one device, grabs that serial number automatically</span>
<span class="c"># If there are multiple devices, shows a chooser with the list of serial numbers</span>
<span class="k">function </span>getSerialNumber<span class="o">()</span> <span class="o">{</span>
<span class="nv">serial_number</span><span class="o">=</span><span class="si">$(</span>get_devices<span class="si">)</span>
<span class="o">}</span>
</code></pre></div></div>
<p>To make it even easier, we can make a convenience function to call through to <code class="language-plaintext highlighter-rouge">getSerialNumber</code> and then launch the <code class="language-plaintext highlighter-rouge">adb</code> command (thanks to my teammate <a href="https://www.linkedin.com/in/aniruddhfichadia/">Ani</a> for suggesting this!):</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Sends an interactive ADB command</span>
<span class="c"># Usage: Use the usual ADB command, replacing `adb` with `adbi`</span>
<span class="k">function </span>adbi<span class="o">()</span> <span class="o">{</span>
getSerialNumber <span class="o">&&</span> adb <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$serial_number</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Applying this to our deeplink <code class="language-plaintext highlighter-rouge">alias</code> (which is now a function because <a href="https://www.shellcheck.net/">Shellcheck</a> will not stop complaining about it):</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Deep links</span>
<span class="k">function </span>deeplink<span class="o">()</span> <span class="o">{</span>
adbi shell am start <span class="nt">-W</span> <span class="nt">-a</span> android.intent.action.VIEW <span class="nt">-d</span> <span class="se">\"</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span><span class="se">\"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>This solution is really adaptible and works well for the <code class="language-plaintext highlighter-rouge">rotatePortrait</code> function too:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function </span>rotatePortrait<span class="o">()</span> <span class="o">{</span>
getSerialNumber
adb <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$serial_number</span><span class="s2">"</span> shell settings put system accelerometer_rotation 0
adb <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$serial_number</span><span class="s2">"</span> shell settings put system user_rotation 0
<span class="o">}</span>
</code></pre></div></div>
<p>Now it only asks us to choose the device once and uses that serial number for all the <code class="language-plaintext highlighter-rouge">adb</code> commands to be executed.</p>
<p>I like this solution a lot for a couple of reasons:</p>
<ul>
<li>itās super easy to update our current aliases, i.e. <code class="language-plaintext highlighter-rouge">s/adb/adbi</code></li>
<li>the syntax is VERY similar to the usual <code class="language-plaintext highlighter-rouge">adb</code> syntax, i.e. <code class="language-plaintext highlighter-rouge">s/adb/adbi</code></li>
</ul>
<p>I think itās super obvious that we have a clear winner here š„šļøāāļø Option 2 it is! And to celebrate, as always, the gist is in <a href="https://gist.github.com/zmdominguez/9a889f1c367e1a21203ce8527c81e612">Github</a>.</p>Zarah DominguezA few weeks ago, I wrote about a script for making adb a little bit more interactive. The script makes the process of running an adb command much smoother if there are multiple devices attached by presenting a chooser. For example, when sending a deeplink: ā ~ deeplink https://zarah.dev Multiple devices found: 1 - R5CR7039LBJ 2 - emulator-5554 3 - emulator-5556 Select device:Making ADB a little bit dynamic š±2023-08-30T00:00:00+00:002023-08-30T00:00:00+00:00https://zarah.dev/2023/08/30/adb-deeplinks<p>Android has a lot of tools for developers and one that has been around for as long as I can remember is <a href="https://developer.android.com/tools/adb">Android Debug Bridge</a> (<code class="language-plaintext highlighter-rouge">adb</code>). It allows you to issue commands to an attached device, such as installing an app or starting an <code class="language-plaintext highlighter-rouge">Activity</code>.</p>
<p>If I want to test deeplinks, for example, I can issue an <code class="language-plaintext highlighter-rouge">adb</code> command that simulates the system sending an <code class="language-plaintext highlighter-rouge">Intent</code> directed to my app:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ adb shell am start <span class="nt">-W</span> <span class="nt">-a</span> android.intent.action.VIEW <span class="nt">-d</span> <span class="s2">"https://zarah.dev"</span>
Starting: Intent <span class="o">{</span> <span class="nv">act</span><span class="o">=</span>android.intent.action.VIEW <span class="nv">dat</span><span class="o">=</span>https://zarah.dev/... <span class="o">}</span>
Status: ok
LaunchState: WARM
Activity: dev.zarah.sdksample/.DetailActivity
TotalTime: 165
WaitTime: 168
Complete
</code></pre></div></div>
<p>I usually test on a real device, but sometimes I have to spin up an emulator to test on a different screen size or OS version, and sometimes I also attach my personal phone to charge. I have lost count of how many times I have tried to run an <code class="language-plaintext highlighter-rouge">adb</code> command and forgot that I have multiple devices attached.</p>
<p>When the deeplink command is sent again in these circumstances:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ adb shell am start <span class="nt">-W</span> <span class="nt">-a</span> android.intent.action.VIEW <span class="nt">-d</span> <span class="s2">"https://zarah.dev"</span>
adb: more than one device/emulator
</code></pre></div></div>
<p>One of the quirks of <code class="language-plaintext highlighter-rouge">adb</code> is that it tells us there is more than one device, but it doesnāt tell us <em>what</em> those devices are. To make the command work again, we need to include the serial number of the target device.</p>
<p>We query for all devices via <code class="language-plaintext highlighter-rouge">adb devices</code> and then add the <code class="language-plaintext highlighter-rouge">-s <SERIAL_NUMBER></code> option when running the command:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ adb devices
List of devices attached
emulator-5554 device
emulator-5556 device
ā ~ adb <span class="nt">-s</span> emulator-5554 shell am start <span class="nt">-W</span> <span class="nt">-a</span> android.intent.action.VIEW <span class="nt">-d</span> <span class="s2">"https://zarah.dev"</span>
Starting: Intent <span class="o">{</span> <span class="nv">act</span><span class="o">=</span>android.intent.action.VIEW <span class="nv">dat</span><span class="o">=</span>https://zarah.dev/... <span class="o">}</span>
Status: ok
LaunchState: WARM
Activity: dev.zarah.sdksample/.DetailActivity
TotalTime: 289
WaitTime: 306
Complete
</code></pre></div></div>
<p>Wouldnāt it be nice if <code class="language-plaintext highlighter-rouge">adb</code> just straight up notifies us of the problem (multiple devices found), asks us how we want to fix the problem (which device should be the target), and then try again?</p>
<p>After years and years of dealing with this, I finally gave in and wrote a script that just does that. š</p>
<p>With a super handy <code class="language-plaintext highlighter-rouge">deeplink</code> alias, I can launch the script and provide it with a URI. If thereās only one device, it issues the command directly:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ deeplink https://zarah.dev
Starting: Intent <span class="o">{</span> <span class="nv">act</span><span class="o">=</span>android.intent.action.VIEW <span class="nv">dat</span><span class="o">=</span>https://zarah.dev/... <span class="o">}</span>
Status: ok
LaunchState: WARM
Activity: dev.zarah.sdksample/.DetailActivity
TotalTime: 165
WaitTime: 168
Complete
</code></pre></div></div>
<p>But when there are multiple devices, it shows the list of devices available and asks for which one to target:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ deeplink https://zarah.dev
Multiple devices found:
1 - R5CR7039LBJ
2 - emulator-5554
3 - emulator-5556
Select device:
</code></pre></div></div>
<p>There is no need to faff about copying serial numbers, as entering the option should be enough. I added an actual device to the mix, and if I want to send the <code class="language-plaintext highlighter-rouge">Intent</code> to that device I can type in <code class="language-plaintext highlighter-rouge">1</code> and press enter:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ deeplink https://zarah.dev
Multiple devices found:
1 - R5CR7039LBJ
2 - emulator-5554
3 - emulator-5556
Select device: 1
Starting: Intent <span class="o">{</span> <span class="nv">act</span><span class="o">=</span>android.intent.action.VIEW <span class="nv">dat</span><span class="o">=</span>https://zarah.dev/... <span class="o">}</span>
Status: ok
LaunchState: WARM
Activity: dev.zarah.sdksample/.DetailActivity
TotalTime: 648
WaitTime: 667
Complete
</code></pre></div></div>
<p>I did talk about using the <code class="language-plaintext highlighter-rouge">deeplink</code> <code class="language-plaintext highlighter-rouge">alias</code> <a href="https://zarah.dev/2022/02/08/android12-deeplinks.html">before</a>, but I have since updated it to run the script instead:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">deeplink</span><span class="o">=</span><span class="s1">'zsh /Users/zarah/scripts/deeplink.sh $1'</span>
</code></pre></div></div>
<h3 id="the-nuts-and-bolts-of-it-">The nuts and bolts of it š©</h3>
<p>There is nothing truly special about how the script works, but it is doing a bunch of RegEx (which should tell you that it took me waaaaaay to long to figure out š).</p>
<p>First, we call <code class="language-plaintext highlighter-rouge">adb devices</code> to figure out how many devices are available:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">all_devices</span><span class="o">=</span><span class="si">$(</span><span class="nb">command </span>adb devices<span class="si">)</span>
<span class="c"># Drop the title ("List of devices attached")</span>
<span class="nv">all_devices</span><span class="o">=</span><span class="k">${</span><span class="nv">all_devices</span><span class="p">#</span><span class="s2">"List of devices attached"</span><span class="k">}</span>
</code></pre></div></div>
<p>Figure out how many recognised devices there are:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">num_matches</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$all_devices</span> | egrep <span class="nt">-o</span> <span class="s2">"([[:alnum:]-]+[[:space:]]+device</span><span class="nv">$)</span><span class="s2">"</span> | <span class="nb">wc</span> <span class="nt">-l</span><span class="si">)</span>
</code></pre></div></div>
<p>If thereās only one device, send the command immediately; otherwise, we need to ask which device to send the command to:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># If there are multiple, ask for which device to send the command to</span>
<span class="k">if</span> <span class="o">[[</span> <span class="nv">$num_matches</span> <span class="nt">-gt</span> 1 <span class="o">]]</span><span class="p">;</span> <span class="k">then
</span>deeplink_with_multiple
<span class="c"># Otherwise just send the ADB command</span>
<span class="k">else
</span><span class="nb">command </span>adb shell am start <span class="nt">-W</span> <span class="nt">-a</span> android.intent.action.VIEW <span class="nt">-d</span> <span class="se">\"</span><span class="nv">$URL</span><span class="se">\"</span>
<span class="k">fi</span>
</code></pre></div></div>
<p>In this case <code class="language-plaintext highlighter-rouge">$URL</code> is the variable that holds the input parameter (the URL passed into the script).</p>
<p>If there are multiple devices, we do more string manipulation to present the list:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Display device serial numbers</span>
<span class="nv">find_matches</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$all_devices</span> | egrep <span class="nt">-io</span> <span class="s2">"([[:alnum:]-]+[[:space:]]+device</span><span class="nv">$)</span><span class="s2">"</span> | <span class="nb">awk</span> <span class="s1">'{print NR " - " $1}'</span><span class="si">)</span>
<span class="nb">printf</span> <span class="s2">"Multiple devices found:</span><span class="se">\n</span><span class="s2">%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$find_matches</span><span class="s2">"</span>
</code></pre></div></div>
<p>Notice the syntax is very similar to the alias I use for displaying the <a href="https://zarah.dev/2021/08/10/magic-reflog.html">recently-checked out branches in git</a>. Thank you 2021 Zarah for figuring that out!</p>
<p>We then ask for the input:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Present chooser</span>
<span class="nb">echo</span> <span class="nt">-n</span> <span class="s2">"Select device: "</span>
<span class="nb">read</span> <span class="nt">-r</span> selected_device
</code></pre></div></div>
<p>Find the matching serial number chosen and issue the command:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Send the ADB command with the serial number</span>
<span class="nv">serial_number</span><span class="o">=</span><span class="si">$(</span><span class="nb">echo</span> <span class="nv">$find_matches</span> | egrep <span class="s2">"</span><span class="k">${</span><span class="nv">selected_device</span><span class="k">}</span><span class="s2"> - (.*)"</span> | <span class="nb">awk</span> <span class="s1">'{print $3}'</span><span class="si">)</span>
<span class="nb">command </span>adb <span class="nt">-s</span> <span class="nv">$serial_number</span> shell am start <span class="nt">-W</span> <span class="nt">-a</span> android.intent.action.VIEW <span class="nt">-d</span> <span class="se">\"</span><span class="nv">$URL</span><span class="se">\"</span>
</code></pre></div></div>
<h3 id="do-this-for-all-the-things-">Do this for all the things! šØ</h3>
<p>The best thing about this script is itās super extensible. By changing the issued <code class="language-plaintext highlighter-rouge">adb</code> commands in the script, I can have this convenience apply to basically any <code class="language-plaintext highlighter-rouge">adb</code> commands I usually use.</p>
<p>It is especially handy for those things that require a bunch of <code class="language-plaintext highlighter-rouge">adb</code> commands, such as <a href="https://developer.android.com/tools/adb#forwardports">forwarding</a> or reversing ports. A bunch of commands mean a bunch of places where <code class="language-plaintext highlighter-rouge">-s <SERIAL_NUMBER></code> needs to be added and letting the script do it means we wonāt miss adding it to any of them:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb <span class="nt">-s</span> <span class="nv">$serial_number</span> wait-for-device <span class="o">&&</span> adb <span class="nt">-s</span> <span class="nv">$serial_number</span> reverse tcp:9000 tcp:9000 <span class="o">&&</span> adb <span class="nt">-s</span> <span class="nv">$serial_number</span> reverse tcp:3000 tcp:3000
</code></pre></div></div>
<p>I am š© at shell scripting (as evidenced by how much time I spent writing this tiny script), but I imagine it may be possible to make this work without having to have one version of the script for each <code class="language-plaintext highlighter-rouge">adb</code> command. Maybe a lookup map with the command name as the key and the <code class="language-plaintext highlighter-rouge">adb</code> command for a single device and the <code class="language-plaintext highlighter-rouge">adb</code> command for multiple devices as the values? Is that even possible? Maybe? Itād be nice.</p>
<p>But for now, the script is <a href="https://gist.github.com/zmdominguez/1b74a2fa6bb027870362a3ca5202a8df">available on Github</a>.</p>Zarah DominguezAndroid has a lot of tools for developers and one that has been around for as long as I can remember is Android Debug Bridge (adb). It allows you to issue commands to an attached device, such as installing an app or starting an Activity.Bundling Things Nice and Pretty š2023-08-21T00:00:00+00:002023-08-21T00:00:00+00:00https://zarah.dev/2023/08/21/bundle-parcel<p>Of all the projects that I have worked on over the years, one thing they all have in common is the need to pass things around. Whether passing stuff to an <code class="language-plaintext highlighter-rouge">Activity</code> as <code class="language-plaintext highlighter-rouge">Intent</code> extras, a <code class="language-plaintext highlighter-rouge">Fragment</code> as arguments or its <code class="language-plaintext highlighter-rouge">onSaveInstanceState</code>, or even a <code class="language-plaintext highlighter-rouge">ViewModel</code>ās <code class="language-plaintext highlighter-rouge">SavedStateHandle</code>, the most common way to do it is through a <a href="https://developer.android.com/reference/android/os/Bundle"><code class="language-plaintext highlighter-rouge">Bundle</code></a>.</p>
<p>An <code class="language-plaintext highlighter-rouge">Activity</code> can accept different types of data through the various <code class="language-plaintext highlighter-rouge">putExtra</code> methods, such as the usual <code class="language-plaintext highlighter-rouge">int</code>, <code class="language-plaintext highlighter-rouge">boolean</code>, <code class="language-plaintext highlighter-rouge">long</code>, etc., array versions of these types, or even <code class="language-plaintext highlighter-rouge">Parcelable</code>s.</p>
<p>Letās take this data class, for example:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">data class</span> <span class="nc">Person</span><span class="p">(</span>
<span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
<span class="kd">val</span> <span class="py">rank</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span>
<span class="c1">// ...other fields omitted</span>
<span class="p">)</span>
</code></pre></div></div>
<p>Say we have another <code class="language-plaintext highlighter-rouge">Activity</code> called <code class="language-plaintext highlighter-rouge">DetailActivity</code> that needs the <code class="language-plaintext highlighter-rouge">Person</code>ās <code class="language-plaintext highlighter-rouge">name</code> and the <code class="language-plaintext highlighter-rouge">rank</code>. We can pass these values individually via the relevant <code class="language-plaintext highlighter-rouge">putExtra</code> calls:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">detailIntent</span> <span class="p">=</span> <span class="nc">Intent</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nc">DetailActivity</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
<span class="n">detailIntent</span><span class="p">.</span><span class="nf">putExtra</span><span class="p">(</span><span class="nc">DetailActivity</span><span class="p">.</span><span class="nc">EXTRA_KEY_NAME</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
<span class="n">detailIntent</span><span class="p">.</span><span class="nf">putExtra</span><span class="p">(</span><span class="nc">DetailActivity</span><span class="p">.</span><span class="nc">EXTRA_KEY_RANK</span><span class="p">,</span> <span class="n">person</span><span class="p">.</span><span class="n">rank</span><span class="p">)</span>
</code></pre></div></div>
<p class="notice"><strong>Note</strong>: In most circumstances, we would need to pass around minimal information such as an <code class="language-plaintext highlighter-rouge">ID</code>. However, there may be instances where we have to deal with more complex structures ā for example, when a user is applying filters to a list. For the purposes of this post, we will deal with multiple properties of a <code class="language-plaintext highlighter-rouge">data class</code>.</p>
<p>Here, I opted to define the <code class="language-plaintext highlighter-rouge">String</code> values for the keys as <code class="language-plaintext highlighter-rouge">const val</code>s in a <code class="language-plaintext highlighter-rouge">companion object</code> in <code class="language-plaintext highlighter-rouge">DetailActivity</code> so I donāt have to type them over and over again:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">DetailActivity</span> <span class="p">:</span> <span class="nc">AppCompatActivity</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>
<span class="k">const</span> <span class="kd">val</span> <span class="py">EXTRA_KEY_NAME</span> <span class="p">=</span> <span class="s">"dev.zarah.person.name"</span>
<span class="k">const</span> <span class="kd">val</span> <span class="py">EXTRA_KEY_RANK</span> <span class="p">=</span> <span class="s">"dev.zarah.person.rank"</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And retrieve them in <code class="language-plaintext highlighter-rouge">DetailActivity</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">fun</span> <span class="nf">onCreate</span><span class="p">(</span><span class="n">savedInstanceState</span><span class="p">:</span> <span class="nc">Bundle</span><span class="p">?)</span> <span class="p">{</span>
<span class="kd">val</span> <span class="py">name</span> <span class="p">=</span> <span class="n">intent</span><span class="p">.</span><span class="nf">getStringExtra</span><span class="p">(</span><span class="nc">EXTRA_KEY_NAME</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">rank</span> <span class="p">=</span> <span class="n">intent</span><span class="p">.</span><span class="nf">getIntExtra</span><span class="p">(</span><span class="nc">EXTRA_KEY_RANK</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This works, but IMHO itās not ideal. For one, we need to be extra careful that we are using the correct <code class="language-plaintext highlighter-rouge">get***Extra</code> call when retrieving the data. If we need to add another value to be passed, we need to change the code in a bunch of places: we need to add a new key in the <code class="language-plaintext highlighter-rouge">companion object</code>, add another <code class="language-plaintext highlighter-rouge">putExtra</code> call in the originating <code class="language-plaintext highlighter-rouge">Activity</code>, and add another <code class="language-plaintext highlighter-rouge">get***Extra</code> call in the receiving <code class="language-plaintext highlighter-rouge">Activity</code>. If for some reason we need to change the type of any one of the extras, we should not forget to change the <code class="language-plaintext highlighter-rouge">get***Extra</code> call. The IDE cannot help us here, and we need to rely on our tests to catch any mismatch.</p>
<p>If we are working with <code class="language-plaintext highlighter-rouge">Fragment</code>s, the idea is similar but we need wrap the values together in a <code class="language-plaintext highlighter-rouge">Bundle</code> before sending them through as <code class="language-plaintext highlighter-rouge">arguments</code>. An <code class="language-plaintext highlighter-rouge">Activity</code> can also accept a <code class="language-plaintext highlighter-rouge">Bundle</code> as an extra, so we can use the <a href="https://developer.android.com/reference/kotlin/androidx/core/os/package-summary#bundleOf(kotlin.Array)"><code class="language-plaintext highlighter-rouge">bundleOf</code> convenience function</a> to do the wrapping up:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">bundle</span> <span class="p">=</span> <span class="nf">bundleOf</span><span class="p">(</span>
<span class="nc">DetailFragment</span><span class="p">.</span><span class="nc">EXTRA_KEY_NAME</span> <span class="n">to</span> <span class="n">person</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
<span class="nc">DetailFragment</span><span class="p">.</span><span class="nc">EXTRA_KEY_RANK</span> <span class="n">to</span> <span class="n">person</span><span class="p">.</span><span class="n">rank</span><span class="p">,</span>
<span class="p">)</span>
<span class="c1">// Passing into a `Fragment`</span>
<span class="kd">val</span> <span class="py">fragment</span> <span class="p">=</span> <span class="nc">DetailFragment</span><span class="p">()</span>
<span class="n">fragment</span><span class="p">.</span><span class="n">arguments</span> <span class="p">=</span> <span class="n">bundle</span>
<span class="c1">// Passing into an `Activity`:</span>
<span class="kd">val</span> <span class="py">detailIntent</span> <span class="p">=</span> <span class="nc">Intent</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nc">DetailActivity</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">)</span>
<span class="n">detailIntent</span><span class="p">.</span><span class="nf">putExtra</span><span class="p">(</span><span class="nc">DetailActivity</span><span class="p">.</span><span class="nc">EXTRA_KEY_AS_BUNDLE</span><span class="p">,</span> <span class="n">bundle</span><span class="p">)</span>
</code></pre></div></div>
<p>I think the <code class="language-plaintext highlighter-rouge">Bundle</code> approach is <em>slightly</em> better for an <code class="language-plaintext highlighter-rouge">Activity</code> because it groups the information into one thing and if we want to refactor the <code class="language-plaintext highlighter-rouge">Activity</code> into a <code class="language-plaintext highlighter-rouge">Fragment</code> in the future, we already have a <code class="language-plaintext highlighter-rouge">Bundle</code> of stuff that we can use. However, we still need to remember to use the correct <code class="language-plaintext highlighter-rouge">get***</code> methods when retrieving values from the <code class="language-plaintext highlighter-rouge">Bundle</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">bundleFromExtra</span> <span class="p">=</span> <span class="nf">requireNotNull</span><span class="p">(</span><span class="n">intent</span><span class="p">.</span><span class="nf">getBundleExtra</span><span class="p">(</span><span class="nc">EXTRA_KEY_AS_BUNDLE</span><span class="p">))</span>
<span class="kd">val</span> <span class="py">nameFromBundle</span> <span class="p">=</span> <span class="n">bundleFromExtra</span><span class="p">.</span><span class="nf">getString</span><span class="p">(</span><span class="nc">EXTRA_KEY_NAME</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">rankFromBundle</span> <span class="p">=</span> <span class="n">bundleFromExtra</span><span class="p">.</span><span class="nf">getInt</span><span class="p">(</span><span class="nc">EXTRA_KEY_RANK</span><span class="p">)</span>
</code></pre></div></div>
<h3 id="parcel-ing-it-up-"><code class="language-plaintext highlighter-rouge">Parcel</code>-ing it up š</h3>
<p>The good news is that we can improve our implementation even more by using a <a href="https://developer.android.com/reference/android/os/Parcelable"><code class="language-plaintext highlighter-rouge">Parcelable</code></a>, which both <code class="language-plaintext highlighter-rouge">Activity</code> and <code class="language-plaintext highlighter-rouge">Fragment</code> accept. I remember in my early days as an Android dev, I did not want to touch <code class="language-plaintext highlighter-rouge">Parcel</code>s with a ten-foot pole. But those days are gone and we now have the <a href="https://developer.android.com/kotlin/parcelize"><code class="language-plaintext highlighter-rouge">Parcelable</code> implementation generator</a> that handles the boilerplate code required by <code class="language-plaintext highlighter-rouge">Parcelable</code>.</p>
<p>Going back to our example above, we can make a data class that would encapsulate the data we need to pass, annotate it with <code class="language-plaintext highlighter-rouge">@Parcelize</code>, and have it implement the <code class="language-plaintext highlighter-rouge">Parcelable</code> interface:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Parcelize</span>
<span class="kd">data class</span> <span class="nc">DetailsExtras</span><span class="p">(</span>
<span class="kd">val</span> <span class="py">name</span><span class="p">:</span> <span class="nc">String</span><span class="p">,</span>
<span class="kd">val</span> <span class="py">rank</span><span class="p">:</span> <span class="nc">Int</span><span class="p">,</span>
<span class="p">)</span> <span class="p">:</span> <span class="nc">Parcelable</span>
</code></pre></div></div>
<p>In some cases, there may not be a need to create a new <code class="language-plaintext highlighter-rouge">data class</code> just for extras or arguments. Annotating the <code class="language-plaintext highlighter-rouge">Person</code> class may work just as well if we need to pass everything that <code class="language-plaintext highlighter-rouge">data class</code> contains. For now, let us assume that there we do not want to pass through other information from <code class="language-plaintext highlighter-rouge">Person</code>, or perhaps we want to cobble together information from different models and thus need a new <code class="language-plaintext highlighter-rouge">data class</code>.</p>
<p>We can make a new instance of this <code class="language-plaintext highlighter-rouge">DetailsExtras</code> <code class="language-plaintext highlighter-rouge">data class</code> so we can pass it to an <code class="language-plaintext highlighter-rouge">Activity</code> or <code class="language-plaintext highlighter-rouge">Fragment</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">detailExtras</span> <span class="p">=</span> <span class="nc">DetailActivity</span><span class="p">.</span><span class="nc">Companion</span><span class="p">.</span><span class="nc">DetailsExtras</span><span class="p">(</span>
<span class="n">name</span> <span class="p">=</span> <span class="n">person</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
<span class="n">rank</span> <span class="p">=</span> <span class="n">person</span><span class="p">.</span><span class="n">rank</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">detailIntent</span><span class="p">.</span><span class="nf">putExtra</span><span class="p">(</span><span class="nc">DetailActivity</span><span class="p">.</span><span class="nc">EXTRA_KEY_AS_PARCEL</span><span class="p">,</span> <span class="n">detailExtras</span><span class="p">)</span>
<span class="nf">startActivity</span><span class="p">(</span><span class="n">detailIntent</span><span class="p">)</span>
</code></pre></div></div>
<p>This is obviously personal preference, but when I need a <code class="language-plaintext highlighter-rouge">data class</code> for encapsulating extras I like putting in a <code class="language-plaintext highlighter-rouge">companion object</code> together with the key for the extra so that they live close together.</p>
<p>Retrieving the values is the same as before, except we only need to remember to retrieve a <code class="language-plaintext highlighter-rouge">Parcelable</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Pre-API33</span>
<span class="kd">val</span> <span class="py">extras</span> <span class="p">=</span> <span class="nf">requireNotNull</span><span class="p">(</span><span class="n">intent</span><span class="p">.</span><span class="n">getParcelableExtra</span><span class="p"><</span><span class="nc">DetailsExtras</span><span class="p">>(</span><span class="nc">EXTRA_KEY_AS_PARCEL</span><span class="p">))</span>
<span class="c1">// API33+</span>
<span class="kd">val</span> <span class="py">extras</span> <span class="p">=</span> <span class="nf">requireNotNull</span><span class="p">(</span><span class="n">intent</span><span class="p">.</span><span class="nf">getParcelableExtra</span><span class="p">(</span><span class="nc">EXTRA_KEY_AS_PARCEL</span><span class="p">,</span> <span class="nc">DetailsExtras</span><span class="o">::</span><span class="k">class</span><span class="p">.</span><span class="n">java</span><span class="p">))</span>
<span class="kd">val</span> <span class="py">name</span> <span class="p">=</span> <span class="n">extras</span><span class="p">.</span><span class="n">name</span>
<span class="kd">val</span> <span class="py">rank</span> <span class="p">=</span> <span class="n">extras</span><span class="p">.</span><span class="n">rank</span>
</code></pre></div></div>
<p>With this approach, we do not have to worry about the types of <code class="language-plaintext highlighter-rouge">name</code> or <code class="language-plaintext highlighter-rouge">rank</code> because Kotlin is smart and can help us figure it out.</p>
<h3 id="adding-more-stuff-to-our-stuff-">Adding more stuff to our stuff š</h3>
<p>What I really like about this approach is that it makes the code really predictable. There is no guessing which values may or may not be there, no guessing what types each of the values are, and any default values can be incorporated into the data class itself.</p>
<p>This also makes our implementation scalable and flexible ā we can even nest other <code class="language-plaintext highlighter-rouge">data class</code>es inside it if we so choose.</p>
<p>But perhaps the biggest benefit of all in my opinion is making the IDE do a lot of the thinking for us. Since we are using a <code class="language-plaintext highlighter-rouge">data class</code>, adding or removing a property (or changing its type) causes the IDE to flag all the places we need to update.</p>
<p>And if thereās one thing I know for sure, itās that the earlier I let the IDE flag any errors before I need to rebuild my project, the better. š</p>Zarah DominguezOf all the projects that I have worked on over the years, one thing they all have in common is the need to pass things around. Whether passing stuff to an Activity as Intent extras, a Fragment as arguments or its onSaveInstanceState, or even a ViewModelās SavedStateHandle, the most common way to do it is through a Bundle.Multi-module Lint Rules Follow Up: Suppressions ā ļø2022-02-15T00:00:00+00:002022-02-15T00:00:00+00:00https://zarah.dev/2022/02/15/deprecated-suppress<p>It has been a hot minute since I posted about <a href="https://zarah.dev/2021/10/04/multi-module-lint.html">writing multi-module Lint rules</a> so itās time for a follow up. Todayās topic: suppressions! A quick recap of where we are:</p>
<p>We have <a href="https://github.com/zmdominguez/lint-rule-samples/blob/main/lint-checks/src/main/java/dev/zarah/lint/checks/DeprecatedColorInXmlDetector.kt">written a Lint rule</a> that checks for usages of deprecated colours (including selectors) in XML files. The rule goes through all modules in the project looking for colours that are contained in any file with the <code class="language-plaintext highlighter-rouge">_deprecated</code> suffix in the filename. We then report usages of those colours as errors. We have also <a href="https://github.com/zmdominguez/lint-rule-samples/blob/main/lint-checks/src/test/java/dev/zarah/lint/checks/DeprecatedColorInXmlDetectorTest.kt">written tests</a> for our Lint rule that cover most (all?) scenarios.</p>
<h3 id="suppression-checks-ļø">Suppression Checks š”ļø</h3>
<p>A key mechanism we employ in our Lint rule is calling <a href="https://github.com/zmdominguez/lint-rule-samples/blob/d7a78ba8c2970121127e55df7db2959e932917ff/lint-checks/src/main/java/dev/zarah/lint/checks/DeprecatedColorInXmlDetector.kt#L109"><code class="language-plaintext highlighter-rouge">getPartialResults</code></a> in the <code class="language-plaintext highlighter-rouge">afterCheckEachProject</code> callback. We use the returned <a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/PartialResult.kt"><code class="language-plaintext highlighter-rouge">PartialResults</code></a> to store:</p>
<ul>
<li>the list of deprecated colours, and</li>
<li>the list of all colour usages</li>
</ul>
<p>in each module (If itās a bit confusing, I highly recommend reading through the <a href="https://zarah.dev/2021/10/04/multi-module-lint.html">OG post</a> and maybe things will make more sense).</p>
<p>The <a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/Context.kt;l=446;drc=f801809cdabf506b19c1b7d19eff16a358469370">KDoc for <code class="language-plaintext highlighter-rouge">getPartialResults</code></a> point out that suppressions are not checked at this point:</p>
<blockquote>
<p>Note that in this case, the lint infrastructure will not automatically look up the error location (since there isnāt one yet) to see if the issue has been suppressed (via annotations, lint.xml and other mechanisms), so you should do this yourself, via the various <code class="language-plaintext highlighter-rouge">LintDriver.isSuppressed</code> methods.</p>
</blockquote>
<p>This presents us with a great opportunity to improve our <code class="language-plaintext highlighter-rouge">DeprecatedColorInXml</code> Lint rule. We donāt even want to <em>consider</em> reporting a colour usage if our Lint rule is suppressed. Since we are parsing an XML file, we can use the <a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintDriver.kt;l=3465;drc=6a64a0c6ff08e0a34226c91a71e775d2c2699ded"><code class="language-plaintext highlighter-rouge">isSuppressed()</code> variant that takes in an <code class="language-plaintext highlighter-rouge">XmlContext</code></a>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">fun</span> <span class="nf">visitAttribute</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">XmlContext</span><span class="p">,</span> <span class="n">attribute</span><span class="p">:</span> <span class="nc">Attr</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// The issue is suppressed for this attribute, skip it</span>
<span class="kd">val</span> <span class="py">isIssueSuppressed</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">driver</span><span class="p">.</span><span class="nf">isSuppressed</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="nc">ISSUE</span><span class="p">,</span> <span class="n">attribute</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="n">isIssueSuppressed</span><span class="p">)</span> <span class="k">return</span>
<span class="c1">// ...</span>
<span class="p">}</span>
<span class="k">override</span> <span class="k">fun</span> <span class="nf">visitElement</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">XmlContext</span><span class="p">,</span> <span class="n">element</span><span class="p">:</span> <span class="nc">Element</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// The issue is suppressed for this element, skip it</span>
<span class="kd">val</span> <span class="py">isIssueSuppressed</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">driver</span><span class="p">.</span><span class="nf">isSuppressed</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="nc">ISSUE</span><span class="p">,</span> <span class="n">element</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="n">isIssueSuppressed</span><span class="p">)</span> <span class="k">return</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>I assume that the suppression checks can also be done in <code class="language-plaintext highlighter-rouge">afterCheckEachProject</code> but why delay when we can bail out early?</p>
<h3 id="tests-">Tests š¬</h3>
<p>With these updates, <a href="https://developer.android.com/studio/write/lint#configuring-lint-checking-in-xml">suppressed Lint issues in XML files</a> will not be reported even if they are missing from the baseline file. We can leverage our <a href="https://github.com/zmdominguez/lint-rule-samples/blob/d7a78ba8c2970121127e55df7db2959e932917ff/lint-checks/src/test/java/dev/zarah/lint/checks/DeprecatedColorInXmlDetectorTest.kt">existing tests</a> to come up with new ones.</p>
<p>Letās write a test for an example layout file using a deprecated colour. We provide the test with two files: one for deprecated colours and another for the layout file. When we suppress the <code class="language-plaintext highlighter-rouge">DeprecatedColorInXml</code> rule in a widget in the layout file, there should not be any reported issues.</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="k">fun</span> <span class="nf">testSuppressedDeprecatedColorInWidget</span><span class="p">()</span> <span class="p">{</span>
<span class="nf">lint</span><span class="p">().</span><span class="nf">files</span><span class="p">(</span>
<span class="nf">xml</span><span class="p">(</span>
<span class="s">"res/values/colors_deprecated.xml"</span><span class="p">,</span>
<span class="s">"""
<resources>
<color name="some_colour">#d6163e</color>
</resources>
"""</span>
<span class="p">).</span><span class="nf">indented</span><span class="p">(),</span>
<span class="nf">xml</span><span class="p">(</span>
<span class="s">"res/layout/layout.xml"</span><span class="p">,</span>
<span class="s">"""
<View xmlns:android="http://schemas.android.com/apk
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/some_colour"
tools:ignore="DeprecatedColorInXml" />
</View>
"""</span>
<span class="p">).</span><span class="nf">indented</span><span class="p">()</span>
<span class="p">)</span>
<span class="p">.</span><span class="nf">testModes</span><span class="p">(</span><span class="nc">TestMode</span><span class="p">.</span><span class="nc">PARTIAL</span><span class="p">)</span>
<span class="p">.</span><span class="nf">run</span><span class="p">()</span>
<span class="p">.</span><span class="nf">expectClean</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>
<p>For completeness, we can also add a test where the suppression is declared in the root element of the layout file and a deprecated colour is used in a widget (i.e. move <code class="language-plaintext highlighter-rouge">tools:ignore="DeprecatedColorInXml"</code> to the <code class="language-plaintext highlighter-rouge">View</code>).</p>
<hr />
<p>As always, the rule updates and the new test cases are <a href="https://github.com/zmdominguez/lint-rule-samples/commit/83f8cfb9345cff346f395a9c8876e153fcc24ab8">in Github</a>.</p>Zarah DominguezIt has been a hot minute since I posted about writing multi-module Lint rules so itās time for a follow up. Todayās topic: suppressions! A quick recap of where we are:Debugging App Links in Android 12 š2022-02-08T00:00:00+00:002022-02-08T00:00:00+00:00https://zarah.dev/2022/02/08/android12-deeplinks<p>I have been working with deeplinks lately and I noticed that quite a few things have changed since I last worked with them. The most important change is quoted in the list of <a href="https://developer.android.com/about/versions/12/behavior-changes-all#web-intent-resolution">Android 12 behaviour changes</a>:</p>
<blockquote>
<p>Starting in Android 12 (API level 31), a generic web intent resolves to an activity in your app <strong>only if your app is approved for the specific domain</strong> contained in that web intent. If your app isnāt approved for the domain, the web intent resolves to the userās default browser app instead.</p>
<footer>Emphasis mine</footer>
</blockquote>
<p>Thereās enough documentation on the Android developer site on how to go about handling this approval. But to recap:</p>
<ul>
<li><a href="https://developer.android.com/training/app-links/deep-linking#adding-filters">Add intent filters in the AndroidManifest file</a></li>
<li><a href="https://developer.android.com/training/app-links/verify-site-associations#add-intent-filters">Make sure <code class="language-plaintext highlighter-rouge">autoVerify</code> is set to <code class="language-plaintext highlighter-rouge">true</code></a></li>
<li><a href="https://developer.android.com/training/app-links/verify-site-associations#web-assoc">Associate your website with your app</a></li>
</ul>
<p>If all goes well, clicking on a link should open the corresponding screen in the app:</p>
<center>
<a href="https://imgur.com/4Kn6N5T"><img src="https://imgur.com/4Kn6N5T.gif" width="320" /></a><br />
<small>Deep linking into the product details screen</small>
</center>
<p>If things do <em>not</em> go well, Google has provided ways to <a href="https://developer.android.com/training/app-links/verify-site-associations#testing">test deeplinks</a>. There are lots of ways to figure out where things went wrong, but they are scattered in different sections. For my sanity, I have collated the steps I have found so that they are all in one place.</p>
<h3 id="website-linking">Website Linking</h3>
<p>If your website is not verified to work with the app, auto-verification will fail. Head on over to the <a href="https://developers.google.com/digital-asset-links/tools/generator">Statement List Generator and Tester</a>, put in the required details, and click on āTest statementā.</p>
<center>
<a href="https://imgur.com/T9J8qI8"><img src="https://i.imgur.com/T9J8qI8.png" title="source: imgur.com" width="450" /></a><br />
<small>Successful linking!</small>
</center>
<p>You can also use the Digital Assets API to confirm that the <code class="language-plaintext highlighter-rouge">assetlinks.json</code> file is properly hosted:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=[YOUR_WEBSITE_URL]&relation=delegate_permission/common.handle_all_urls
</code></pre></div></div>
<p>Remember that verification should pass for <strong>all</strong> hosts declared in the <code class="language-plaintext highlighter-rouge">AndroidManifest</code> file on Android 11 and below, so make sure to test each of them.</p>
<p>If any of these tests fail, review the <a href="https://developers.google.com/digital-asset-links/v1/create-statement">Digital Asset Links documentation</a> and make sure that the file is formatted properly.</p>
<p class="notice--info">We found out the hard way that the value for your certificateās <code class="language-plaintext highlighter-rouge">sha256_cert_fingerprints</code> in <code class="language-plaintext highlighter-rouge">assetlinks.json</code> <strong>SHOULD</strong> be in ALL CAPS</p>
<p>(Thanks to <a href="https://twitter.com/bentrengrove">Ben Trengrove</a> for debugging that issue with me!)</p>
<p>On the device-side of things, we can also check the status of domain verification:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell pm get-app-links <span class="o">[</span>YOUR_PACKAGE_NAME]
</code></pre></div></div>
<p>This will show results similar to this:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> com.woolworths:
ID: fb789c89-1d2e-403a-be0c-a8871a8e5b76
Signatures: <span class="o">[</span>41:0F:9A:43:72:FC:C0:76:BD:90:AC:C4:A0:6F:96:D5:24:CC:1E:69:2E:79:18:1F:05:0C:78:21:8C:39:27:D5]
Domain verification state:
woolworths.app.link: verified
woolworths-alternate.app.link: verified
www.woolworths.com.au: verified
</code></pre></div></div>
<p>There are various states for domain verification. Check out the <a href="https://developer.android.com/training/app-links/verify-site-associations#review-results">documentation</a> for what each of those may mean.</p>
<h3 id="user-permissions">User Permissions</h3>
<p>If everything on the website side of things is setup properly, check that the user has allowed opening your appās supported links.</p>
<p>The easiest way to do this is to use the ADB command to check the domain verification status and add <a href="https://developer.android.com/training/app-links/verify-site-associations#user-prompt-command-line-program">flags to show the userās side of things</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell pm get-app-links <span class="nt">--user</span> cur <span class="o">[</span>YOUR_PACKAGE_NAME]
</code></pre></div></div>
<p>Running this command will spit out the verification status and if the user has given your app permission to open declared URLs:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code> com.woolworths:
ID: fb789c89-1d2e-403a-be0c-a8871a8e5b76
Signatures: <span class="o">[</span>41:0F:9A:43:72:FC:C0:76:BD:90:AC:C4:A0:6F:96:D5:24:CC:1E:69:2E:79:18:1F:05:0C:78:21:8C:39:27:D5]
Domain verification state:
woolworths.app.link: verified
woolworths-alternate.app.link: verified
www.woolworths.com.au: verified
User 0:
Verification <span class="nb">link </span>handling allowed: <span class="nb">true
</span>Selection state:
Disabled:
woolworths.app.link
woolworths-alternate.app.link
www.woolworths.com.au
</code></pre></div></div>
<p>To see the status of <em>ALL</em> apps on the device, run the following ADB command to <a href="https://developer.android.com/training/app-links/verify-site-associations#check-link-policies">check all link policies</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell dumpsys package d
// OR
adb shell dumpsys package domain-preferred-apps
</code></pre></div></div>
<p>I find the information this shows to be very interesting! Maybe thatās just me though, Iām weird like that. :nerd_face:</p>
<p>Note that even if auto-verification fails, the user can manually allow your app to open links. Take this output for the debug variant of our app for example:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>com.woolworths.debug:
ID: 99e87cda-e951-4e7a-ba6a-894a31718add
Signatures: <span class="o">[</span>AF:35:FE:62:F8:11:02:16:8D:B4:7F:15:91:A3:9B:43:0E:9C:B0:93:F7:57:AC:99:B2:FC:19:2E:C1:A8:E3:96]
Domain verification state:
woolworths-alternate.test-app.link: legacy_failure
www.woolworths.com.au: verified
woolworths.test-app.link: legacy_failure
User 0:
Verification <span class="nb">link </span>handling allowed: <span class="nb">true
</span>Selection state:
Enabled:
woolworths-alternate.test-app.link
woolworths.test-app.link
Disabled:
www.woolworths.com.au
</code></pre></div></div>
<p>Despite two hosts failing the verification process:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>woolworths-alternate.test-app.link: legacy_failure
woolworths.test-app.link: legacy_failure
</code></pre></div></div>
<p>I can go into the appās settings and manually approve these URLs:</p>
<center>
<a href="https://i.imgur.com/OYEKHYO"><img src="https://i.imgur.com/OYEKHYO.png" title="Screenshot showing links" width="320" /></a><br />
<small>Manual permission for supported links</small>
</center>
<h3 id="resetting-verification">Resetting Verification</h3>
<p>There are also ADB commands to facilitate going through the whole validation process.</p>
<p>First <a href="https://developer.android.com/training/app-links/verify-site-associations#reset-state">reset the app links state</a> of the app:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell pm set-app-links <span class="nt">--package</span> <span class="o">[</span>YOUR_PACKAGE_NAME] 0 all
</code></pre></div></div>
<p>Then <a href="https://developer.android.com/training/app-links/verify-site-associations#invoke-domain-verification">manually trigger re-verification</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell pm verify-app-links <span class="nt">--re-verify</span> <span class="o">[</span>YOUR_PACKAGE_NAME]
</code></pre></div></div>
<p>If you want to test out the auto-verification process but do not target Android 12 yet, it can be <a href="https://developer.android.com/training/app-links/verify-site-associations#support-updated-domain-verification">enabled for your app</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell am compat <span class="nb">enable </span>175408749 <span class="o">[</span>YOUR_PACKAGE_NAME]
</code></pre></div></div>
<h3 id="testing-intents">Testing Intents</h3>
<p>Finally, to ensure that we have correctly configured the Intent filters in the <code class="language-plaintext highlighter-rouge">AndroidManifest.xml</code> file and our app can open intended links, <a href="https://developer.android.com/training/app-links/verify-site-associations#auto-verification">send an implicit Intent via ADB</a>:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell am start <span class="nt">-a</span> android.intent.action.VIEW <span class="nt">-c</span> android.intent.category.BROWSABLE <span class="nt">-d</span> <span class="s2">"[URL_HERE]"</span>
</code></pre></div></div>
<p>Since Iām lazy and thatās long command to remember, I added an alias for it:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">deeplink</span><span class="o">=</span><span class="s1">'() { adb shell am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "$1" ;}'</span>
</code></pre></div></div>
<p>So I can do this:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā ~ deeplink https://www.woolworths.com.au/shop/productdetails/670560
Starting: Intent <span class="o">{</span> <span class="nv">act</span><span class="o">=</span>android.intent.action.VIEW <span class="nb">cat</span><span class="o">=[</span>android.intent.category.BROWSABLE] <span class="nv">dat</span><span class="o">=</span>https://www.woolworths.com.au/... <span class="o">}</span>
</code></pre></div></div>
<h3 id="install-time-logs">Install-time Logs</h3>
<p>Back in 2017, I <a href="https://zarah.dev/2017/01/20/testing-autoverify.html">wrote about another way</a> to troubleshoot <code class="language-plaintext highlighter-rouge">autoVerify</code> . You would need to keep an eye on Logcat for the domain verification logs. For our debug variant, these logs look like this:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>I/IntentFilterIntentOp: Verifying IntentFilter. verificationId:180 scheme:<span class="s2">"https"</span> hosts:<span class="s2">"woolworths-alternate.test-app.link www.woolworths.com.au woolworths.test-app.link"</span> package:<span class="s2">"com.woolworths.debug"</span><span class="nb">.</span> <span class="o">[</span>CONTEXT <span class="nv">service_id</span><span class="o">=</span>244 <span class="o">]</span>
I/AppLinksUtilsV1: Legacy cross-profile verification enabled <span class="o">[</span>CONTEXT <span class="nv">service_id</span><span class="o">=</span>244 <span class="o">]</span>
I/SingleHostAsyncVerifier: Verification result: checking <span class="k">for </span>a statement with <span class="nb">source</span> <span class="c"># cfkq@55fed08a, relation delegate_permission/common.handle_all_urls, and target # cfkq@7ce31cea --> true. [CONTEXT service_id=244 ]</span>
I/SingleHostAsyncVerifier: Verification result: checking <span class="k">for </span>a statement with <span class="nb">source</span> <span class="c"># cfkq@5c3d4ef1, relation delegate_permission/common.handle_all_urls, and target # cfkq@7ce31cea --> false. [CONTEXT service_id=244 ]</span>
I/SingleHostAsyncVerifier: Verification result: checking <span class="k">for </span>a statement with <span class="nb">source</span> <span class="c"># cfkq@9705d4b3, relation delegate_permission/common.handle_all_urls, and target # cfkq@7ce31cea --> false. [CONTEXT service_id=244 ]</span>
I/IntentFilterIntentOp: Verification 180 complete. Success:false. Failed hosts:woolworths-alternate.test-app.link,woolworths.test-app.link. <span class="o">[</span>CONTEXT <span class="nv">service_id</span><span class="o">=</span>244 <span class="o">]</span>
</code></pre></div></div>
<p>It looks like the output formatting has changed since 2017 and the individual URLs are not cleartext anymore (for example, <code class="language-plaintext highlighter-rouge">cfkq@55fed08a</code>). Thereās really not much reason to look for these logs aside from checking that <em>some</em> form of auto-verification is happening. The ADB commands weāve gone through in the previous sections show the same information in a much more readable format.</p>
<hr />
<p>Unfortunately, it is difficult to ascertain the inner workings of domain verification. Hopefully the steps outlined here help narrow down possible causes for when your app links fail to cooperate. Good luck and happy (app) linking! :handshake:</p>Zarah DominguezI have been working with deeplinks lately and I noticed that quite a few things have changed since I last worked with them. The most important change is quoted in the list of Android 12 behaviour changes:š£ PSA: Disabling mapping file uploads with Crashlytics2022-01-18T00:00:00+00:002022-01-18T00:00:00+00:00https://zarah.dev/2022/01/18/crashlytics-deobfuscate<p>One of the more famous crash reporting tools used in Android development is probably <a href="https://firebase.google.com/docs/crashlytics">Crashlytics</a>. It offers up a lot of insight into an appās performance ā from device characteristics to insights on issue commonalities. If, like my current project, <a href="https://developer.android.com/studio/build/shrink-code#obfuscate">obfuscation</a> is enabled in an app, Crashlytics has a Gradle plugin that uploads the mapping file so that we end up with readable crash reports.</p>
<p>There are times though where I may not want to upload the mapping file, like if I am just debugging an issue locally. According to the <a href="https://firebase.google.com/docs/crashlytics/get-deobfuscated-reports?platform=android">official documentation</a>, the way to do this is to add this configuration to the <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> file:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>firebaseCrashlytics {
mappingFileUploadEnabled = false
}
</code></pre></div></div>
<p>However, when I tried it with v2.8.1 of the Crashlytics Gradle plugin, I get an unresolved reference error:</p>
<center>
<a href="https://imgur.com/GXTHLXx"><img src="https://i.imgur.com/GXTHLXx.png" title="source: imgur.com" width="450" /></a><br />
<small>Unresolved reference error</small>
</center>
<p>The correct syntax for Kotlin Gradle DSL is somewhat buried in a comment in this <a href="https://github.com/firebase/firebase-android-sdk/issues/2665">Github issue</a>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
buildTypes{
getByName("debug") {
...
configure<CrashlyticsExtension> {
mappingFileUploadEnabled = false
}
}
}
</code></pre></div></div>
<p>Itās unfortunate that the official documentation hasnāt been updated in the eight months since this issue has been known (apparently it appeared in v2.6.0 of the plugin), but such is life. :crying_cat_face:</p>Zarah DominguezOne of the more famous crash reporting tools used in Android development is probably Crashlytics. It offers up a lot of insight into an appās performance ā from device characteristics to insights on issue commonalities. If, like my current project, obfuscation is enabled in an app, Crashlytics has a Gradle plugin that uploads the mapping file so that we end up with readable crash reports.XML Parsing in Lint: Things Are Not What They Seem š¦¹āāļø2021-12-03T00:00:00+00:002021-12-03T00:00:00+00:00https://zarah.dev/2021/12/03/xml-parsing<p>About a year ago, I wrote <a href="https://zarah.dev/2020/11/19/todo-detector.html">about including quickfixes for Lint rules</a>. Quick fixes appear on the context menu when Lint flags an error and allows developers to quickly address the issue. They can be applied by clicking on the link at the bottom of the dialog or pressing ALT+ENTER (ā„ + ā©) and then choosing the fix.</p>
<center>
<a href="https://imgur.com/KCknsbR"><img src="https://i.imgur.com/KCknsbR.png" title="source: imgur.com" width="450" /></a><br />
<small>Hovering over the error brings up a dialog with the quickfix option available</small>
</center>
<center>
<a href="https://imgur.com/ykg8LiM"><img src="https://i.imgur.com/ykg8LiM.png" title="source: imgur.com" width="450" /></a><br />
<small>Pressing ALT+ENTER on the error brings up a menu with available options</small>
</center>
<p>The images above show the rule we have in our project for data binding expression formatting. Applying the quickfix results into putting one whitespace in between the braces and the expression itself:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>android:text="@{ label }"
</code></pre></div></div>
<p>On the outset, it looks like writing this quickfix is pretty straightforward ā look for the opening brace (<code class="language-plaintext highlighter-rouge">{</code>), insert a space if needed, put back the expression, insert a space if needed, put back the closing brace (<code class="language-plaintext highlighter-rouge">}</code>). But say we have this XML layout:
<script src="https://gist.github.com/zmdominguez/afaa8b4a8577054888fb690c9bbe5f43.js"></script>
(This is an overly simplified example for illustration purposes, so keep your comments to yourself)</p>
<p>Notice how in line 17, the <code class="language-plaintext highlighter-rouge">&&</code> characters are escaped (<code class="language-plaintext highlighter-rouge">&amp;&amp;</code>). This is because whilst <a href="https://developer.android.com/topic/libraries/data-binding/expressions#common_features">data binding allows the usage of logical operators</a> like <code class="language-plaintext highlighter-rouge">&&</code>, <a href="https://www.w3.org/TR/xml/#syntax">some characters should always be escaped</a> for our XML file to be syntactically correct.</p>
<h3 id="the-rule-straight_ruler">The rule :straight_ruler:</h3>
<p>Our Lint rule checks all attributes in an XML layout file:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">fun</span> <span class="nf">getApplicableAttributes</span><span class="p">():</span> <span class="nc">Collection</span><span class="p"><</span><span class="nc">String</span><span class="p">>?</span> <span class="p">{</span>
<span class="k">return</span> <span class="nc">XmlScannerConstants</span><span class="p">.</span><span class="nc">ALL</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And when an attribute is encountered, our callback gets triggered:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">override</span> <span class="k">fun</span> <span class="nf">visitAttribute</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">XmlContext</span><span class="p">,</span> <span class="n">attribute</span><span class="p">:</span> <span class="nc">Attr</span><span class="p">)</span>
</code></pre></div></div>
<p>We can then check if the attributeās value is a properly formatted data binding expression:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// An `@` character, an optional `=` character, an opening brace `{`, a space, the expression, a space, a closing brace `}`</span>
<span class="kd">val</span> <span class="py">validPattern</span> <span class="p">=</span> <span class="nc">Regex</span><span class="p">(</span><span class="s">"@=?\\{\\s.*\\s}"</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="nf">isDataBindingExpression</span><span class="p">(</span><span class="n">attributeValue</span><span class="p">)</span> <span class="p">&&</span> <span class="p">!</span><span class="n">attributeValue</span><span class="p">.</span><span class="nf">matches</span><span class="p">(</span><span class="n">validPattern</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// Report the issue</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In our example layout file above, our Lint rule will flag that line 17 is improperly formatted when it visits the <code class="language-plaintext highlighter-rouge">app:visible</code> attribute:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>app:visible="@{hasValue <span class="ni">&amp;&amp;</span> isFeatureOn}"
</code></pre></div></div>
<p>The <a href="https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-637646024"><code class="language-plaintext highlighter-rouge">Attr</code> interface</a> inherits from the <a href="https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-1950641247"><code class="language-plaintext highlighter-rouge">Node</code> interface</a>, so there are some methods that we can use to figure out information about the attribute. For Lint in particular, there is a method <a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/XmlParser.kt;l=137;drc=4395c75d91d34b08dc393f701cd8d82e4c3bb3aa"><code class="language-plaintext highlighter-rouge">getValueLocation</code></a> which we can theoretically use to get the <code class="language-plaintext highlighter-rouge">Location</code> for this nodeās value.</p>
<p class="notice--warning">Remember, <code class="language-plaintext highlighter-rouge">Location</code> is important because it lets us tell Lint where to put the squiggly red lines to highlight the error and also the place in the file where we want to replace the incorrect value.</p>
<h3 id="i-feel-a-but-coming-raising_hand">I feel a āButā¦ā coming :raising_hand:</h3>
<p>Using the <code class="language-plaintext highlighter-rouge">Location</code> returned by <code class="language-plaintext highlighter-rouge">getValueLocation</code>, we expect the whole value part of the attribute to be highlighted. Letās go ahead and use it to report the issue before we build our quickfix:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nf">isDataBindingExpression</span><span class="p">(</span><span class="n">attributeValue</span><span class="p">)</span> <span class="p">&&</span> <span class="p">!</span><span class="n">attributeValue</span><span class="p">.</span><span class="nf">matches</span><span class="p">(</span><span class="n">validPattern</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// Report the issue</span>
<span class="n">context</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span>
<span class="n">issue</span> <span class="p">=</span> <span class="nc">DatabindingExpressionFormatDetector</span><span class="p">.</span><span class="nc">ISSUE</span><span class="p">,</span>
<span class="n">scope</span> <span class="p">=</span> <span class="n">attribute</span><span class="p">,</span>
<span class="n">location</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">getValueLocation</span><span class="p">(</span><span class="n">attribute</span><span class="p">),</span>
<span class="n">message</span> <span class="p">=</span> <span class="s">"Please put one whitespace between the braces and the expression"</span>
<span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If we run our test for this layout file, this is what we get.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>res/layout/layout.xml:17: Warning: Please put one whitespace between the braces and the expression [DatabindingExpressionFormat]
app:visible="@{hasValue &amp;&amp; isFeatureOn}" />
~~~~~~~~~~~~~~~~~~~~~~~~~~
0 errors, 1 warnings
</code></pre></div></div>
<p>Not quite right. :thinking: To understand what is going on, letās look at what happens in the <code class="language-plaintext highlighter-rouge">getValueLocation</code> implementation. We can see that somewhere in there, t<a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/cli/src/main/java/com/android/tools/lint/LintCliXmlParser.java;l=215;drc=1d01e75bf984568ecdf198cd35bfe05c8b0cce9f">he <code class="language-plaintext highlighter-rouge">LintClient</code> retrieves the value of the node</a>:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="n">length</span> <span class="o">=</span> <span class="n">node</span><span class="o">.</span><span class="na">getValue</span><span class="o">().</span><span class="na">length</span><span class="o">();</span>
</code></pre></div></div>
<p>Now if we go back to the <a href="https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-637646024">W3 documentation on <code class="language-plaintext highlighter-rouge">Attr</code></a>:</p>
<blockquote>
<p>The following table gives some examples of the relations between the attribute value in the original document (parsed attribute), the value as exposed in the DOM, and the serialization of the value:</p>
<table>
<thead>
<tr>
<th style="text-align: center">Examples</th>
<th style="text-align: center">Parsed attribute value</th>
<th style="text-align: center">Initial <code class="language-plaintext highlighter-rouge">Attr.value</code></th>
<th style="text-align: center">Serialized attribute value</th>
<th>Ā </th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center">Character reference</td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"x&#178;=5"</code></td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"xĀ²=5"</code></td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"x&#178;=5"</code></td>
<td>Ā </td>
</tr>
<tr>
<td style="text-align: center">Built-in character entity</td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"y&lt;6"</code></td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"y<6"</code></td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"y&lt;6"</code></td>
<td>Ā </td>
</tr>
<tr>
<td style="text-align: center">Literal newline between</td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"x=5&#10;y=6"</code></td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"x=5 y=6"</code></td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"x=5&#10;y=6"</code></td>
<td>Ā </td>
</tr>
<tr>
<td style="text-align: center">Normalized newline between</td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"x=5 y=6"</code></td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"x=5 y=6"</code></td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge">"x=5 y=6"</code></td>
<td>Ā </td>
</tr>
<tr>
<td style="text-align: center">Entity e with literal newline</td>
<td style="text-align: center"><code class="language-plaintext highlighter-rouge"><!ENTITY e '...&#10;...'> [...]> "x=5&e;y=6"</code></td>
<td style="text-align: center">Dependent on Implementation and Load Options</td>
<td style="text-align: center">Dependent on Implementation and Load/Save Options</td>
<td>Ā </td>
</tr>
</tbody>
</table>
</blockquote>
<p>This is important because it says that if there are escaped characters, the resolved characters will be returned when we get the attributeās value.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>val attrValue = node.getValue() // returns "@{hasValue && isFeatureOn}"
</code></pre></div></div>
<p>And we can actually see the effects of this. The red squiggly lines are off by eight characters:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&amp;&amp; -> 10 characters
&& -> 2 characters
</code></pre></div></div>
<h3 id="finding-the-right-location-compass">Finding the right <code class="language-plaintext highlighter-rouge">Location</code> :compass:</h3>
<p>This means that we need to find the correct <code class="language-plaintext highlighter-rouge">Location</code> on our own.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// This is the contents of the whole XML file
val rawText = context.getContents() ?: return Pair(null, null)
// Find where the node bounds
val nodeStart = context.parser.getNodeStartOffset(context, attribute)
val nodeEnd = context.parser.getNodeEndOffset(context, attribute)
// The full contents of the node, including the attribute name (i.e. `app:visible="@{hasValue &amp;&amp; isFeatureOn}"`)
val rawNodeText = rawText.substring(nodeStart, nodeEnd)
</code></pre></div></div>
<p>Letās now find the offset of the start of the expression (remember we cannot rely on the length of <code class="language-plaintext highlighter-rouge">attribute.value</code> because of escaped characters). Since we are working with databinding expressions, we can rely on the syntax to find where the value starts:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Find the databinding prefix used
val prefixExpression = if (rawNodeText.contains(SdkConstants.PREFIX_TWOWAY_BINDING_EXPR)) {
SdkConstants.PREFIX_TWOWAY_BINDING_EXPR
} else SdkConstants.PREFIX_BINDING_EXPR
// First character after the opening `"` in the attribute value
val attributeValueStart = rawNodeText.indexOf(prefixExpression)
</code></pre></div></div>
<p>Now that we know where the attributeās value starts and where the whole node ends, we can finally construct an accurate <code class="language-plaintext highlighter-rouge">Location</code> for the attribute value:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// We cannot use `context.parser.getValueLocation()` since there may be escaped characters</span>
<span class="c1">// Get the Location value for the actual expression within the file (not including the quotation marks)</span>
<span class="kd">val</span> <span class="py">attributeValueLocation</span> <span class="p">=</span> <span class="nc">Location</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="n">file</span><span class="p">,</span> <span class="n">rawText</span><span class="p">,</span> <span class="n">nodeStart</span> <span class="p">+</span> <span class="n">attributeValueStart</span><span class="p">,</span> <span class="n">nodeEnd</span> <span class="p">-</span> <span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>
<h3 id="the-quickfix-woman_mechanic">The quickfix :woman_mechanic:</h3>
<p>When reporting a Lint issue, we can provide our users a quickfix which enables to, uhm, quickly fix the reported issue. For this rule, if either the leading and/or trailing space in the expression is missing, our quickfix will insert those into the file.</p>
<p>Lint has a bunch of <a href="http://googlesamples.github.io/android-custom-lint-rules/api-guide.md.html#addingquickfixes/availablefixes">available types of fixes</a> that can help us do what we want. In this case, I opted to use <code class="language-plaintext highlighter-rouge">replace()</code> and do a straight up string replacement. Of course, as with anything Lint, there are multiple ways to do this (including using regular expressions), but I chose the path of least resistance.</p>
<p>We can now use the <code class="language-plaintext highlighter-rouge">Location</code> information we used earlier to write our quickfix:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Get the actual databinding expression (i.e., what is between `{` and `}`)</span>
<span class="kd">val</span> <span class="py">expressionStart</span> <span class="p">=</span> <span class="n">attributeValueStart</span> <span class="p">+</span> <span class="n">prefixExpression</span><span class="p">.</span><span class="n">length</span> <span class="c1">// length of the expression marker</span>
<span class="kd">val</span> <span class="py">expressionEnd</span> <span class="p">=</span> <span class="n">rawNodeText</span><span class="p">.</span><span class="nf">lastIndexOf</span><span class="p">(</span><span class="s">"}"</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">rawExpressionValue</span> <span class="p">=</span> <span class="n">rawNodeText</span><span class="p">.</span><span class="nf">substring</span><span class="p">(</span><span class="n">expressionStart</span><span class="p">,</span> <span class="n">expressionEnd</span><span class="p">)</span>
<span class="c1">// Formulate the replacement</span>
<span class="kd">val</span> <span class="py">replacementText</span> <span class="p">=</span> <span class="s">"$prefixExpression ${rawExpressionValue.trim()} }"</span>
<span class="c1">// Build the quickfix</span>
<span class="kd">val</span> <span class="py">quickfix</span> <span class="p">=</span> <span class="nf">fix</span><span class="p">()</span>
<span class="p">.</span><span class="nf">name</span><span class="p">(</span><span class="s">"Fix databinding expression formatting"</span><span class="p">)</span>
<span class="p">.</span><span class="nf">replace</span><span class="p">()</span>
<span class="p">.</span><span class="nf">range</span><span class="p">(</span><span class="n">attributeValueLocation</span><span class="p">)</span>
<span class="p">.</span><span class="nf">with</span><span class="p">(</span><span class="n">replacementText</span><span class="p">)</span>
<span class="p">.</span><span class="nf">build</span><span class="p">()</span>
</code></pre></div></div>
<p>And with that, weāre done! The full source code for <a href="https://github.com/zmdominguez/lint-rule-samples/blob/main/lint-checks/src/main/java/dev/zarah/lint/checks/DatabindingExpressionFormatDetector.kt">this Detector</a> as well as the <a href="https://github.com/zmdominguez/lint-rule-samples/blob/main/lint-checks/src/test/java/dev/zarah/lint/checks/DatabindingExpressionFormatDetectorTest.kt">tests</a> are on <a href="https://github.com/zmdominguez/lint-rule-samples">Github</a>.</p>Zarah DominguezAbout a year ago, I wrote about including quickfixes for Lint rules. Quick fixes appear on the context menu when Lint flags an error and allows developers to quickly address the issue. They can be applied by clicking on the link at the bottom of the dialog or pressing ALT+ENTER (ā„ + ā©) and then choosing the fix.Lazy dev: Indexed Branch Switching š³2021-11-15T00:00:00+00:002021-11-15T00:00:00+00:00https://zarah.dev/2021/11/15/co-branch-alias<p>Back in August, I <a href="https://zarah.dev/2021/08/10/magic-reflog.html">wrote about making an alias</a> for finding the five most recent branches I have checked out by filtering out <code class="language-plaintext highlighter-rouge">git reflog</code> entries.</p>
<p>Hereās how the output looks like for the alias:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā sdk_sandbox git:<span class="o">(</span>feature/app-shortcuts<span class="o">)</span> ā grefb
1 - main
2 - feature/clickable-spans
3 - main
4 - task/update-gradle
5 - task/update-to-mdc
</code></pre></div></div>
<p>The alias has been really useful and I have been using it a lot, but the command to <em>actually</em> switch branches is a bit difficult to type.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā sdk_sandbox git:<span class="o">(</span>feature/app-shortcuts<span class="o">)</span> ā gco @<span class="o">{</span><span class="nt">-3</span><span class="o">}</span>
Switched to branch <span class="s1">'main'</span>
Your branch is up to <span class="nb">date </span>with <span class="s1">'origin/main'</span><span class="nb">.</span>
</code></pre></div></div>
<p>So today I added a new alias that will put in the command for me:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">alias </span><span class="nv">gcobr</span><span class="o">=</span><span class="s1">'() { gco @{-$1} ;}'</span>
</code></pre></div></div>
<p>This means I can just enter the index of the branch I want to go back to without having to worry about the syntax :tada: :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ā sdk_sandbox git:<span class="o">(</span>feature/app-shortcuts<span class="o">)</span> ā gcobr 3
Switched to branch <span class="s1">'main'</span>
Your branch is up to <span class="nb">date </span>with <span class="s1">'origin/main'</span><span class="nb">.</span>
</code></pre></div></div>Zarah DominguezBack in August, I wrote about making an alias for finding the five most recent branches I have checked out by filtering out git reflog entries.Multi-module Lint Rules: Tests š§Ŗ2021-10-05T00:00:00+00:002021-10-05T00:00:00+00:00https://zarah.dev/2021/10/05/multi-module-lint-test<p>In my <a href="https://zarah.dev/2021/10/04/multi-module-lint.html">previous post</a>, I talked about how to write a Lint rule that gathers information from different modules before performing a final analysis to determine if there are errors.</p>
<p>Writing tests for files that reside in the same module generally follow the same principles as outlined in my post <a href="https://zarah.dev/2020/11/20/todo-test.html">Enforcing Team Rules with Lint: Tests</a> so for now we will be focusing on writing tests for a multi-module setup.</p>
<p>We will write a test with the following set-up:</p>
<ul>
<li>two modules, a library and an app module</li>
<li>the library module holds a <code class="language-plaintext highlighter-rouge">colors_deprecated.xml</code> file</li>
<li>the app module holds a layout file that uses one of the deprecated colours</li>
</ul>
<p>Letās set up the <code class="language-plaintext highlighter-rouge">colors_deprecated.xml</code> file:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">DEPRECATED_COLOUR_FILE</span><span class="p">:</span> <span class="nc">TestFile</span> <span class="p">=</span> <span class="nf">xml</span><span class="p">(</span>
<span class="s">"res/values/colors_deprecated.xml"</span><span class="p">,</span>
<span class="s">"""
<resources>
<color name="red_error">#d6163e</color>
<color name="another_value">#d6163e</color>
<color name="and_another_value">#d6163e</color>
</resources>
"""</span>
<span class="p">).</span><span class="nf">indented</span><span class="p">()</span>
</code></pre></div></div>
<p>And the layout file consuming the colour:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">VIEW_WITH_DEPRECATED_COLOUR</span><span class="p">:</span> <span class="nc">TestFile</span> <span class="p">=</span> <span class="nf">xml</span><span class="p">(</span>
<span class="s">"res/layout/layout.xml"</span><span class="p">,</span>
<span class="s">"""
<View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/red_error" />
"""</span>
<span class="p">).</span><span class="nf">indented</span><span class="p">()</span>
</code></pre></div></div>
<p>Next we need to set up our projects (aka modules). Lintās mechanism for setting modules up in tests is via <a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-tests/src/main/java/com/android/tools/lint/checks/infrastructure/ProjectDescription.kt"><code class="language-plaintext highlighter-rouge">ProjectDescription</code></a> (oddly enough this is missing in the API guide as well :crying_cat_face: ). Letās set up the library module first:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">deprecatedModule</span> <span class="p">=</span> <span class="nc">ProjectDescription</span><span class="p">()</span>
<span class="p">.</span><span class="nf">name</span><span class="p">(</span><span class="s">"deprecated-library"</span><span class="p">)</span>
<span class="p">.</span><span class="nf">type</span><span class="p">(</span><span class="nc">ProjectDescription</span><span class="p">.</span><span class="nc">Type</span><span class="p">.</span><span class="nc">LIBRARY</span><span class="p">)</span>
<span class="p">.</span><span class="nf">files</span><span class="p">(</span><span class="nc">DEPRECATED_COLOUR_FILE</span><span class="p">)</span>
</code></pre></div></div>
<p>Here we provide the module name, declare it as a library, and include the files that should be in the module.</p>
<p>Letās then set up our app module:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">appModule</span> <span class="p">=</span> <span class="nc">ProjectDescription</span><span class="p">()</span>
<span class="p">.</span><span class="nf">name</span><span class="p">(</span><span class="s">"app"</span><span class="p">)</span>
<span class="p">.</span><span class="nf">files</span><span class="p">(</span><span class="nc">VIEW_WITH_DEPRECATED_COLOUR</span><span class="p">)</span>
<span class="p">.</span><span class="nf">dependsOn</span><span class="p">(</span><span class="n">deprecatedModule</span><span class="p">)</span>
</code></pre></div></div>
<p>Note that we call <code class="language-plaintext highlighter-rouge">dependsOn</code> here to link the two modules together.</p>
<p>All thatās left to do now is to write the bit that runs the test:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">lint</span><span class="p">()</span>
<span class="c1">// Add modules to the test task</span>
<span class="p">.</span><span class="nf">projects</span><span class="p">(</span><span class="n">deprecatedModule</span><span class="p">,</span> <span class="n">appModule</span><span class="p">)</span>
<span class="c1">// Tells Lint to analyse all modules first before reporting issues</span>
<span class="p">.</span><span class="nf">testModes</span><span class="p">(</span><span class="nc">TestMode</span><span class="p">.</span><span class="nc">PARTIAL</span><span class="p">)</span>
<span class="p">.</span><span class="nf">run</span><span class="p">()</span>
<span class="p">.</span><span class="nf">expect</span><span class="p">(</span>
<span class="s">"""
res/layout/layout.xml:4: Error: Deprecated colours should not be used [DeprecatedColorInXml]
android:background="@color/red_error" />
~~~~~~~~~~~~~~~~
1 errors, 0 warnings
"""</span>
<span class="p">)</span>
</code></pre></div></div>
<p>Using the same principles we can write tests for analysing transitive dependencies as well. For the full test suite for this Lint rule, check out the source code on <a href="https://github.com/zmdominguez/lint-rule-samples/blob/main/lint-checks/src/test/java/dev/zarah/lint/checks/DeprecatedColorInXmlDetectorTest.kt">Github</a>.</p>Zarah DominguezIn my previous post, I talked about how to write a Lint rule that gathers information from different modules before performing a final analysis to determine if there are errors.