<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://zarah.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://zarah.dev/" rel="alternate" type="text/html" /><updated>2025-12-02T13:07:03+00:00</updated><id>https://zarah.dev/feed.xml</id><title type="html">Zarah Dominguez</title><subtitle>An Android Love Affair</subtitle><author><name>Zarah Dominguez</name></author><entry><title type="html">Lint Me: Test Sources 🖇️</title><link href="https://zarah.dev/2025/12/02/lint-tests.html" rel="alternate" type="text/html" title="Lint Me: Test Sources 🖇️" /><published>2025-12-02T00:00:00+00:00</published><updated>2025-12-02T00:00:00+00:00</updated><id>https://zarah.dev/2025/12/02/lint-tests</id><content type="html" xml:base="https://zarah.dev/2025/12/02/lint-tests.html"><![CDATA[<p>It has been a year, which means it is once again time to re-examine our <a href="/2024/07/22/todo-detector-v2.html">TODO Lint rule</a>.</p>

<p>TL;DR: The rule checks if a <code class="language-plaintext highlighter-rouge">TODO</code> includes mandatory information – an assignee and a date.</p>

<figure class="align-center">
<a href="https://imgur.com/yPGU7eX"><img src="https://i.imgur.com/yPGU7eX.png" width="400" /></a>
</figure>

<p>We have also <a href="/2024/07/24/lintfix-alternatives.html">explored providing alternatives</a> when suggesting quick fixes:</p>

<figure class="align-center">
<a href="https://imgur.com/VTc8m4F"><img src="https://i.imgur.com/VTc8m4F.gif" /></a> 
<figcaption>A Lint rule with alternative fixes</figcaption>
</figure>

<p>Today we will add another feature: enforcing the Lint rule in test files.</p>

<p>Say we have this test file with an invalid <code class="language-plaintext highlighter-rouge">TODO</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">MainActivityTest</span> <span class="p">{</span>

    <span class="c1">// TODO write tests</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A quick and simple modification is to update the <code class="language-plaintext highlighter-rouge">lint</code> configuration in the
project’s <code class="language-plaintext highlighter-rouge">build.gradle.kts</code> file with <code class="language-plaintext highlighter-rouge">checkTestSources</code>:</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">// ...</span>
    <span class="n">checkTestSources</span> <span class="p">=</span> <span class="k">true</span>
<span class="p">}</span>
</code></pre></div></div>

<p>From the <a href="https://developer.android.com/reference/tools/gradle-api/8.3/null/com/android/build/api/dsl/Lint#setCheckTestSources(kotlin.Boolean)">documentation</a> and 
the comment in the sample DSL:</p>
<blockquote>
  <p>// Normally most lint checks are not run on test sources (except the checks<br />
// dedicated to looking for mistakes in unit or instrumentation tests, unless<br />
// ignoreTestSources is true). You can turn on normal lint checking in all<br />
// sources with the following flag, false by default</p>
</blockquote>

<p>For the purposes of today’s discussion, I do not want to enable <em>all</em> 
the Lint rules to run on my test sources <strong><em>but</em></strong> I <em>do</em> want the TODO 
Detector to.</p>

<p>One of the parameters in a Lint rule’s <code class="language-plaintext highlighter-rouge">Implementation</code> is the <code class="language-plaintext highlighter-rouge">Scope</code>. From the 
<a href="https://github.com/googlesamples/android-custom-lint-rules/blob/main/docs/api-guide/terminology.md.html#L92">documentation</a>:</p>
<blockquote>
  <p><code class="language-plaintext highlighter-rouge">Scope</code> is an enum which lists various types of files that a detector may want to analyze. <br />
For example, there is a scope for XML files, there is a scope for Java and Kotlin files, there is a scope for .class files, and so on.<br />
Typically lint cares about which <strong>set</strong> of scopes apply, so most of the APIs take an <code class="language-plaintext highlighter-rouge">EnumSet&lt;Scope&gt;</code>, but we’ll often refer to this as just “the scope” instead of the “scope set”.</p>
</blockquote>

<p>Read more about <code class="language-plaintext highlighter-rouge">Scope</code>s <a href="https://googlesamples.github.io/android-custom-lint-rules/api-guide.html#writingalintcheck:basics/scopes">here</a>. The “various types of files” the documentation refer to are listed <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/Scope.kt;l=1?q=scope&amp;sq=&amp;ss=android-studio">here</a>.</p>

<p>To recap, this is how the current <code class="language-plaintext highlighter-rouge">Implementation</code> of the TODO Detector <a href="https://github.com/zmdominguez/lint-rule-samples/blob/f51654a2e6f4697a58da9b4414d8c0d6eca87274/lint-checks/src/main/java/dev/zarah/lint/checks/TodoDetector.kt#L294">looks like</a>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kd">val</span> <span class="py">IMPLEMENTATION</span> <span class="p">=</span> <span class="nc">Implementation</span><span class="p">(</span>
    <span class="nc">TodoDetector</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="nc">Scope</span><span class="p">.</span><span class="nc">JAVA_FILE_SCOPE</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The name <code class="language-plaintext highlighter-rouge">Scope.JAVA_FILE_SCOPE</code> confused me for a little bit – the documentation states an <code class="language-plaintext highlighter-rouge">EnumSet</code>
is required but the name implies it is a simple <code class="language-plaintext highlighter-rouge">Scope</code>. In actual truth, <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/Scope.kt;l=267?q=scope&amp;ss=android-studio">it <em>is</em> an <code class="language-plaintext highlighter-rouge">EnumSet</code></a>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">JAVA_FILE_SCOPE</span><span class="p">:</span> <span class="nc">EnumSet</span><span class="p">&lt;</span><span class="nc">Scope</span><span class="p">&gt;</span> <span class="p">=</span> <span class="nc">EnumSet</span><span class="p">.</span><span class="nf">of</span><span class="p">(</span><span class="nc">JAVA_FILE</span><span class="p">)</span>
</code></pre></div></div>

<p>So to enable inspection of test sources, we need to add the more aptly-named <code class="language-plaintext highlighter-rouge">Scope.TEST_SOURCES</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kd">val</span> <span class="py">IMPLEMENTATION</span> <span class="p">=</span> <span class="nc">Implementation</span><span class="p">(</span>
    <span class="nc">TodoDetector</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="nc">EnumSet</span><span class="p">.</span><span class="nf">of</span><span class="p">(</span><span class="nc">Scope</span><span class="p">.</span><span class="nc">JAVA_FILE</span><span class="p">,</span> <span class="nc">Scope</span><span class="p">.</span><span class="nc">TEST_SOURCES</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>After rebuilding the project to pick up the Lint rule changes, errors
in test sources should now be flagged:</p>

<figure class="align-center">
<a href="https://imgur.com/IpoW0hE"><img src="https://i.imgur.com/IpoW0hE.png" width="400" /></a>
<figcaption>Inspected Test Source</figcaption>
</figure>

<p>For completion, we should add a test for this new configuration. The API
has changed a bit since my previous post on unit testing Lint rules <a href="/2020/11/20/todo-test.html">post</a>,
but the idea is the same – provide a <code class="language-plaintext highlighter-rouge">TestFile</code> the unit tests can run in.
Read more about <code class="language-plaintext highlighter-rouge">TestFile</code>s <a href="https://googlesamples.github.io/android-custom-lint-rules/api-guide.html#lintcheckunittesting/testfiles">in the API Guide</a>.</p>

<p>Note that when creating a <code class="language-plaintext highlighter-rouge">TestFile</code>, there is a <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/LintDetectorTest.java;l=381?q=LintDetectorTest&amp;ss=android-studio">constructor that takes a <code class="language-plaintext highlighter-rouge">to</code> parameter</a>:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@NonNull</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="nc">TestFile</span> <span class="nf">kotlin</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">String</span> <span class="n">to</span><span class="o">,</span> <span class="nd">@NonNull</span> <span class="nd">@Language</span><span class="o">(</span><span class="s">"kotlin"</span><span class="o">)</span> <span class="nc">String</span> <span class="n">source</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="nc">TestFiles</span><span class="o">.</span><span class="na">kotlin</span><span class="o">(</span><span class="n">to</span><span class="o">,</span> <span class="n">source</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>where <code class="language-plaintext highlighter-rouge">to</code> is the fully qualified path of the <code class="language-plaintext highlighter-rouge">source</code>.</p>

<p>I haven’t found any direct documentation on this, but this is exactly
what we need to indicate to Lint that a file is a test source. From one of the 
<a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/OpenForTestingDetectorTest.kt;l=68">platform Lint rules</a>, 
it looks like adding a <code class="language-plaintext highlighter-rouge">test</code> prefix is sufficient:</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="nf">files</span><span class="p">(</span>
    <span class="nf">kotlin</span><span class="p">(</span>
        <span class="s">"test/test/pkg/TestClass.kt"</span><span class="p">,</span>
        <span class="s">"""
        package test.pkg
        class TestClass {
            // TODO-Zarah Some comments
        }
    """</span>
    <span class="p">).</span><span class="nf">indented</span><span class="p">()</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The existing unit tests for this rule are pretty comprehensive, so for this
change I opted to only add a missing date test. If this test passes it follows
that the rule works and all other scenarios would pass as well.</p>

<p>As always, the updates to the Detector and the tests are available on <a href="https://github.com/zmdominguez/lint-rule-samples/pull/11">GitHub</a>.</p>

<hr />

<p>To read my past posts on this topic, check out the posts <a href="https://zarah.dev/tags/">tagged with Lint</a>, some
of which are linked below:</p>
<ul>
  <li><a href="/2020/11/18/todo-lint.html">Anatomy of a Lint Issue</a></li>
  <li><a href="/2020/11/20/todo-test.html">Detectors Unit Testing</a></li>
  <li><a href="/2021/10/04/multi-module-lint.html">Multi-module Rules</a></li>
  <li><a href="/2021/10/05/multi-module-lint-test.html">Multi-module Testing</a></li>
  <li><a href="/2024/07/24/lintfix-alternatives.html">Providing LintFix Alternatives</a></li>
</ul>

<p>Here are some first-party resources for Lint:</p>
<ul>
  <li><a href="https://googlesamples.github.io/android-custom-lint-rules/api-guide.md.html">Lint API Guide</a></li>
  <li><a href="http://googlesamples.github.io/android-custom-lint-rules/user-guide.html">Lint User Guide</a></li>
  <li><a href="https://github.com/googlesamples/android-custom-lint-rules">Lint sample project</a></li>
  <li><a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/">Some framework rules</a></li>
</ul>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="lint" /><summary type="html"><![CDATA[It has been a year, which means it is once again time to re-examine our TODO Lint rule.]]></summary></entry><entry><title type="html">Lint Revisit: Providing Alternatives 🧙‍♀️</title><link href="https://zarah.dev/2024/07/24/lintfix-alternatives.html" rel="alternate" type="text/html" title="Lint Revisit: Providing Alternatives 🧙‍♀️" /><published>2024-07-24T00:00:00+00:00</published><updated>2024-07-24T00:00:00+00:00</updated><id>https://zarah.dev/2024/07/24/lintfix-alternatives</id><content type="html" xml:base="https://zarah.dev/2024/07/24/lintfix-alternatives.html"><![CDATA[<p>In my <a href="/2024/07/22/todo-detector-v2.html">previous post</a>, we
updated our TODO Detector to be more flexible. It is also easily extensible
so that if we want to include more parameters or perhaps add more checks,
we can follow the existing pattern and modify it.</p>

<p>For example, what if instead of the date being in parentheses, we want a
reference to a JIRA ticket or a GitHub issue instead. Furthermore, what if
we want to restrict these issues to a set of pre-defined project-specific
prefixes? What if we want to surface those prefixes in the auto-fix options? 
Something like this maybe?</p>

<figure class="align-center">
<a href="https://imgur.com/VTc8m4F"><img src="https://i.imgur.com/VTc8m4F.gif" /></a> 
<figcaption>A Lint rule with alternative fixes</figcaption>
</figure>

<p>Super cool right?</p>

<h3 id="lets-make-it-happen-">Let’s make it happen 👩‍🍳</h3>

<p>Say we only allow tickets with an <code class="language-plaintext highlighter-rouge">ABCD</code> or <code class="language-plaintext highlighter-rouge">XYZ</code> prefix like in the example above. 
We first define an enum containing these prefixes:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">enum</span> <span class="kd">class</span> <span class="nc">VALID_TICKET_PREFIXES</span> <span class="p">{</span>
    <span class="nc">ABCD</span><span class="p">,</span>
    <span class="nc">XYZ</span><span class="p">,</span>
    <span class="p">;</span>
    <span class="k">companion</span> <span class="k">object</span> <span class="p">{</span>
        <span class="k">fun</span> <span class="nf">allPrefixes</span><span class="p">()</span> <span class="p">=</span> <span class="nc">VALID_TICKET_PREFIXES</span><span class="p">.</span><span class="n">entries</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="n">it</span><span class="p">.</span><span class="n">name</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And use that to construct our RegEx pattern:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Only accept valid prefixes followed by a dash and one or more numbers</span>
<span class="kd">val</span> <span class="py">ticketPattern</span> <span class="p">=</span> <span class="nc">VALID_TICKET_PREFIXES</span><span class="p">.</span><span class="nf">allPrefixes</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">joinToString</span><span class="p">(</span><span class="n">separator</span> <span class="p">=</span> <span class="s">"|"</span><span class="p">)</span> <span class="p">{</span> <span class="n">prefix</span> <span class="p">-&gt;</span>
        <span class="s">"$prefix-[0-9]+"</span>
    <span class="p">}</span>
    
<span class="kd">val</span> <span class="py">COMPLETE_PATTERN_REGEX</span> <span class="p">=</span> <span class="s">""".*TODO-(?&lt;MATCH_KEY_ASSIGNEE&gt;[^:\(\s-]+) \((?&lt;$MATCH_KEY_TICKET&gt;$ticketPattern)\):.*"""</span><span class="p">.</span><span class="nf">toRegex</span><span class="p">()</span>
</code></pre></div></div>

<p>We can still use the same checks as we do for the date:</p>
<ul>
  <li>check if there is anything enclosed in parentheses,</li>
  <li>check if the value contained in <code class="language-plaintext highlighter-rouge">MATCH_KEY_TICKET</code> starts with any of the valid prefixes</li>
</ul>

<p>When we report the issue, we can then include the valid prefixes in the issue
explanation to help users figure out what went wrong:</p>

<figure class="align-center">
<a href="https://imgur.com/I9y0Vyy"><img src="https://i.imgur.com/I9y0Vyy.png" width="500" /></a>
<figcaption>Issue explanation</figcaption>
</figure>

<h3 id="offering-more-help-">Offering more help 🛟</h3>

<p>However, to make our rule even more helpful, we can include available options in 
our <code class="language-plaintext highlighter-rouge">LintFix</code>:</p>

<figure class="align-center">
<a href="https://imgur.com/jF2CIQU"><img src="https://i.imgur.com/jF2CIQU.png" width="400" /></a>
<figcaption>Alternatives as intentions</figcaption>
</figure>

<p>This is done by adding <a href="https://googlesamples.github.io/android-custom-lint-rules/api-guide.md.html#addingquickfixes/combiningfixes"><code class="language-plaintext highlighter-rouge">alternatives()</code></a> 
to our <code class="language-plaintext highlighter-rouge">LintFix</code>:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Create a fix with alternatives</span>
<span class="kd">val</span> <span class="py">ticketAlternatives</span> <span class="p">=</span> <span class="nf">fix</span><span class="p">().</span><span class="nf">alternatives</span><span class="p">()</span>

<span class="nc">VALID_TICKET_PREFIXES</span><span class="p">.</span><span class="nf">allPrefixes</span><span class="p">().</span><span class="nf">forEach</span> <span class="p">{</span> <span class="n">prefix</span> <span class="p">-&gt;</span>
    <span class="kd">val</span> <span class="py">replacement</span> <span class="p">=</span> <span class="s">"$prefix-"</span>
    
    <span class="c1">// Create an individual fix suggesting each valid prefix</span>
    <span class="kd">val</span> <span class="py">prefixFix</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">"Add $prefix ticket"</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">dateLocation</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">select</span><span class="p">(</span><span class="s">"($replacement)"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">with</span><span class="p">(</span><span class="n">replacement</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
        
    <span class="c1">// Add this fix to our alternatives</span>
    <span class="n">ticketAlternatives</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">prefixFix</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In addition to putting in the prefix, I wanted to put the cursor after the dash
to make it <em>even easier</em> for users. This way, all that’s needed to be done is
put in the actual ticket number. I cannot figure out how to do that though, so
for now the newly-added prefix would be highlighted (similar to what would happen
if you click and drag the cursor).</p>

<p>Selecting a bunch of text can be done using, you guessed it, <code class="language-plaintext highlighter-rouge">select()</code> which 
expects a <code class="language-plaintext highlighter-rouge">@RegExp</code>. According to the documentation:</p>

<blockquote>
  <p>Sets a pattern to select; if it contains parentheses, group(1) will be selected. To just set the caret, use an empty group.</p>
</blockquote>

<p>According to this I should be able to set the caret, but I cannot figure out how. I
tried searching for more documentation and in the platform rules but was, alas,
unsuccessful. Do you know how to do it? Let me know please! 🙏</p>

<p>Anyway, now we can use this fix when the <code class="language-plaintext highlighter-rouge">Incident</code> is reported:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">incident</span> <span class="p">=</span> <span class="nc">Incident</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">issue</span><span class="p">(</span><span class="nc">MISSING_OR_INVALID_PREFIX</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">location</span><span class="p">(</span><span class="n">problemLocation</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">message</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">fix</span><span class="p">(</span><span class="n">ticketAlternatives</span><span class="p">.</span><span class="nf">build</span><span class="p">())</span>
<span class="n">context</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="n">incident</span><span class="p">)</span>
</code></pre></div></div>

<p>Isn’t it neat? 😍</p>

<h3 id="testing-alternatives-">Testing alternatives 🧪</h3>

<p>And yes! It IS possible to test these alternatives! The syntax is similar to
how we test a <code class="language-plaintext highlighter-rouge">LintFix</code>, but repeated for each alternative provided:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="nf">expectFixDiffs</span><span class="p">(</span>
    <span class="s">"""
         Fix for src/test/pkg/TestClass1.kt line 3: Add ABCD ticket:
         @@ -3 +3
         -     // TODO-Zarah (): Some comments
         +     // TODO-Zarah ([ABCD-]|): Some comments
         Fix for src/test/pkg/TestClass1.kt line 3: Add XYZ ticket:
         @@ -3 +3
         -     // TODO-Zarah (): Some comments
         +     // TODO-Zarah ([XYZ-]|): Some comments
    """</span><span class="p">.</span><span class="nf">trimIndent</span><span class="p">()</span>
<span class="p">)</span>
</code></pre></div></div>

<p>The only weird-looking thing here is the syntax for testing the <code class="language-plaintext highlighter-rouge">select()</code> directive
we included in the fix:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TODO-Zarah ([ABCD-]|): Some comments</span>

<span class="c1">// TODO-Zarah ([XYZ-]|): Some comments</span>
</code></pre></div></div>

<p>What this means is that any matches to the <code class="language-plaintext highlighter-rouge">@RegExp</code> we pass into <code class="language-plaintext highlighter-rouge">select()</code> must
be enclosed between <code class="language-plaintext highlighter-rouge">[</code> and <code class="language-plaintext highlighter-rouge">]</code>. I assumed the pipe (<code class="language-plaintext highlighter-rouge">|</code>) is meant to indicate
where the caret is? Maybe? 🤔</p>

<p>We’ll leave this here for now, unless inspiration hits me and we can spiffify this 
rule even more. 👋</p>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="lint" /><summary type="html"><![CDATA[In my previous post, we updated our TODO Detector to be more flexible. It is also easily extensible so that if we want to include more parameters or perhaps add more checks, we can follow the existing pattern and modify it.]]></summary></entry><entry><title type="html">Lint Revisit: TODO Detector v2</title><link href="https://zarah.dev/2024/07/22/todo-detector-v2.html" rel="alternate" type="text/html" title="Lint Revisit: TODO Detector v2" /><published>2024-07-22T00:00:00+00:00</published><updated>2024-07-22T00:00:00+00:00</updated><id>https://zarah.dev/2024/07/22/todo-detector-v2</id><content type="html" xml:base="https://zarah.dev/2024/07/22/todo-detector-v2.html"><![CDATA[<p>A few years ago, I wrote about writing <a href="/2020/11/19/todo-detector.html">a Lint rule</a> to validate the format of <code class="language-plaintext highlighter-rouge">TODO</code> comments. Whilst I find that 
Lint is still difficult to grok, I have since learnt a little bit more that I feel a revisit of this rule is
warranted.</p>

<p>To recap, the rule enforces that all <code class="language-plaintext highlighter-rouge">TODO</code>s must follow the format:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TODO-[ASSIGNEE] (DATE_TODAY): Some comments</span>
</code></pre></div></div>

<p>The RegEx to check if a <code class="language-plaintext highlighter-rouge">TODO</code> is valid or not is a bit loosey-goosey, and just checks for a very
generic pattern:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/*
 Comment should follow the pattern:
    // = Two backward slashes
    \\s+ = one or more whitespaces
    TODO- = literal "TODO-"
    \\w* = zero or more word characters
    \\s+ = one or more whitespaces
    \\( = an open parentheses
    d{8} = eight numeric characters
    \\) = a close parentheses
    : = literal ":"
    .* = zero or more of any character
*/</span>
<span class="nc">Regex</span><span class="p">(</span><span class="s">"//\\s+TODO-\\w*\\s+\\(\\d{8}\\):.*"</span><span class="p">)</span>
</code></pre></div></div>

<p>In addition, the auto-fixes in the current version of the rule is a bit naive. For one, it assumes 
that any <code class="language-plaintext highlighter-rouge">TODO</code> does not have an assignee and the auto-fix will blindly tack on the assignee and 
today’s date.</p>

<p>For this iteration, the rule will be expanded to:</p>
<ul>
  <li>do separate checks for the assignee and the date,</li>
  <li>change the date format to <code class="language-plaintext highlighter-rouge">yyyy-MM-dd</code> to make it easier to read,</li>
  <li>re-use either field if it already exists,</li>
  <li>update tests to include <code class="language-plaintext highlighter-rouge">LintFix</code>es</li>
</ul>

<p>I had some new tricks up my sleeve this time, including named <code class="language-plaintext highlighter-rouge">MatchGroup</code>s in RegEx (read more about that
<a href="/2024/07/21/regex-groups.html">here</a>).</p>

<h3 id="being-more-specific-">Being more specific 📍</h3>

<p>As a developer, nothing annoys me more than a very vague error message. They are unhelpful and provide
no feedback on what actually caused the error, nor steps on how to fix it.</p>

<p>One change in this version is to split out the checks for the assignee and the date. This allows
us to provide a more specific error message to the user:</p>

<p><a href="https://imgur.com/T5I3TKZ"><img src="https://i.imgur.com/T5I3TKZ.png" width="400" /></a></p>

<p>First up is an update to the Regex to check for the complete pattern:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/*
 Comment should follow the pattern:
    .* = zero or more of any character
    TODO- = literal "TODO-"
    (?&lt;MATCH_KEY_ASSIGNEE&gt;[^:\(\s-]+) = assignee capturing group
        [^:\(\s-]+ = one or more of any character that is not a ":", "(", whitespace, or "-"
     = literal " "
    \( = an open parenthesis
    (?&lt;$MATCH_KEY_DATE&gt;20[0-9]{2}-[01][0-9]-[0-3][0-9]) = date capturing group
        20[0-9]{2}-[01][0-9]-[0-3][0-9] = accepts a four-digit year, a two-digit month, and a two-digit day
                (yes technically it will allow a month value of "00" but let's deal with that next time)
    \) = a close parenthesis
    : = literal ":"
    .* = zero or more of any character
 */</span>
<span class="kd">val</span> <span class="py">COMPLETE_PATTERN_REGEX</span> <span class="p">=</span> <span class="s">""".*TODO-(?&lt;MATCH_KEY_ASSIGNEE&gt;[^:\(\s-]+) \((?&lt;$MATCH_KEY_DATE&gt;20[0-9]{2}-[01][0-9]-[0-3][0-9])\):.*"""</span><span class="p">.</span><span class="nf">toRegex</span><span class="p">()</span>
</code></pre></div></div>

<p>If for one reason or another the comment does not match the pattern, a cascading set of checks are
done and any issue reported as soon as they are encountered:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MISSING_DATE: Date is totally absent, or in the wrong place</span>
<span class="kd">var</span> <span class="py">issueFound</span> <span class="p">=</span> <span class="nf">reportDateIssue</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">comment</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="n">issueFound</span><span class="p">)</span> <span class="k">return</span>

<span class="c1">// MISSING_ASSIGNEE: Assignee is totally absent</span>
<span class="n">issueFound</span> <span class="p">=</span> <span class="nf">reportAssigneeIssue</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">comment</span><span class="p">)</span>
<span class="k">if</span> <span class="p">(</span><span class="n">issueFound</span><span class="p">)</span> <span class="k">return</span>

<span class="c1">// All other issues fall through to here, like if all elements are there but in the wrong order</span>
<span class="kd">val</span> <span class="py">incident</span> <span class="p">=</span> <span class="nc">Incident</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">issue</span><span class="p">(</span><span class="nc">IMPROPER_FORMAT</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">location</span><span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="nf">getLocation</span><span class="p">(</span><span class="n">comment</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">message</span><span class="p">(</span><span class="s">"Improper format"</span><span class="p">)</span>

<span class="c1">// Only suggest the fix for non-block comments</span>
<span class="c1">// Block comments are trickier to figure out, something to implement for the future!</span>
<span class="k">if</span> <span class="p">(</span><span class="n">comment</span><span class="p">.</span><span class="n">sourcePsi</span><span class="p">.</span><span class="n">elementType</span> <span class="p">!=</span> <span class="nc">KtTokens</span><span class="p">.</span><span class="nc">BLOCK_COMMENT</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">incident</span><span class="p">.</span><span class="nf">fix</span><span class="p">(</span><span class="nf">createFix</span><span class="p">(</span><span class="n">comment</span><span class="p">))</span>
<span class="p">}</span>
<span class="n">context</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="n">incident</span><span class="p">)</span>
</code></pre></div></div>

<p>Each kind of error is differentiated via individual <code class="language-plaintext highlighter-rouge">Issue</code> definitions <a href="#issue-definitions">shown here</a>.</p>

<h3 id="reporting-date-issues-️">Reporting date issues 🗓️</h3>

<p>Instead of a simple validation for eight consecutive digits, we now do a three-part check:</p>
<ul>
  <li>if there is nothing enclosed in parentheses, report missing date</li>
  <li>if there are empty parentheses (i.e. <code class="language-plaintext highlighter-rouge">()</code>), report missing date</li>
  <li>if there are values in parentheses, check if it is a valid date and report if not</li>
</ul>

<p>First, we verify if there is anything at all enclosed in parentheses:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Capture everything between the first opening parenthesis and the </span>
<span class="c1">// last closing parenthesis</span>
<span class="kd">val</span> <span class="py">inParensPattern</span> <span class="p">=</span> <span class="s">""".*TODO.*\((?&lt;$MATCH_KEY_DATE&gt;[^\)]*)\).*"""</span><span class="p">.</span><span class="nf">toRegex</span><span class="p">()</span>
<span class="kd">val</span> <span class="py">allInParentheses</span> <span class="p">=</span> <span class="n">inParensPattern</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">commentText</span><span class="p">)</span><span class="o">?.</span><span class="n">groups</span>

<span class="c1">// If there is nothing at all, we can conclude the date is missing</span>
<span class="k">if</span> <span class="p">(</span><span class="n">allInParentheses</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>

    <span class="kd">val</span> <span class="py">incident</span> <span class="p">=</span> <span class="nc">Incident</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">issue</span><span class="p">(</span><span class="nc">MISSING_OR_INVALID_DATE</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">location</span><span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="nf">getLocation</span><span class="p">(</span><span class="n">comment</span><span class="p">))</span>
        <span class="p">.</span><span class="nf">message</span><span class="p">(</span><span class="s">"Missing date"</span><span class="p">)</span>
    
    <span class="n">context</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="n">incident</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If the comment <em>does</em> have parentheses, we check the value contained within via the named
capturing group <code class="language-plaintext highlighter-rouge">MATCH_KEY_DATE</code>.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">dateMatch</span> <span class="p">=</span> <span class="n">inParensMatches</span><span class="p">[</span><span class="nc">MATCH_KEY_DATE</span><span class="p">]</span>
<span class="kd">val</span> <span class="py">parensValue</span> <span class="p">=</span> <span class="nf">requireNotNull</span><span class="p">(</span><span class="n">dateMatch</span><span class="p">).</span><span class="n">value</span>
<span class="kd">val</span> <span class="py">message</span> <span class="p">=</span> <span class="k">when</span> <span class="p">{</span>
    <span class="n">parensValue</span> <span class="p">==</span> <span class="s">""</span> <span class="p">-&gt;</span> <span class="s">"Missing date"</span>
    <span class="p">!</span><span class="nf">isValidDate</span><span class="p">(</span><span class="n">parensValue</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="s">"Invalid date"</span>
    <span class="k">else</span> <span class="p">-&gt;</span> <span class="k">null</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In the snippet above, <code class="language-plaintext highlighter-rouge">isValidDate</code> checks if the value in parentheses follows the date format required
and is within the range defined in <code class="language-plaintext highlighter-rouge">COMPLETE_PATTERN_REGEX</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">fun</span> <span class="nf">isValidDate</span><span class="p">(</span><span class="n">dateString</span><span class="p">:</span> <span class="nc">String</span><span class="p">):</span> <span class="nc">Boolean</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">providedDate</span> <span class="p">=</span> <span class="nc">LocalDate</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">dateString</span><span class="p">,</span> <span class="nc">DateTimeFormatter</span><span class="p">.</span><span class="nf">ofPattern</span><span class="p">(</span><span class="nc">DATE_PATTERN</span><span class="p">))</span>
        <span class="kd">val</span> <span class="py">providedYear</span> <span class="p">=</span> <span class="n">providedDate</span><span class="p">.</span><span class="n">year</span>
        <span class="k">return</span> <span class="n">providedYear</span> <span class="k">in</span> <span class="mi">2024</span><span class="o">..</span><span class="mi">2099</span>
    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="n">e</span><span class="p">:</span> <span class="nc">DateTimeParseException</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="k">false</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If there is an error, we can use the <code class="language-plaintext highlighter-rouge">IntRange</code> contained in <code class="language-plaintext highlighter-rouge">dateMatch</code> to show the red squiggly lines
over the specific error, like so:</p>

<p><a href="https://imgur.com/yPGU7eX"><img src="https://i.imgur.com/yPGU7eX.png" width="400" /></a></p>

<p>To do this, we need to figure out the exact <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/Location.kt"><code class="language-plaintext highlighter-rouge">Location</code></a>
that we want to highlight. We can calculate this from two pieces of information we already know:</p>
<ul>
  <li>the <code class="language-plaintext highlighter-rouge">Location</code> of the comment within the file</li>
  <li>the <code class="language-plaintext highlighter-rouge">IntRange</code> of the value in parentheses within the comment</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">commentStartOffset</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">getLocation</span><span class="p">(</span><span class="n">comment</span><span class="p">).</span><span class="n">start</span><span class="o">?.</span><span class="n">offset</span> <span class="o">?:</span> <span class="mi">0</span>
<span class="kd">val</span> <span class="py">startLocation</span> <span class="p">=</span> <span class="n">commentStartOffset</span> <span class="p">+</span> <span class="n">dateMatch</span><span class="p">.</span><span class="n">range</span><span class="p">.</span><span class="n">first</span>
<span class="kd">val</span> <span class="py">endLocation</span> <span class="p">=</span> <span class="n">commentStartOffset</span> <span class="p">+</span> <span class="n">dateMatch</span><span class="p">.</span><span class="n">range</span><span class="p">.</span><span class="n">last</span>

<span class="c1">// The actual `Location` of the date value</span>
<span class="kd">val</span> <span class="py">dateLocation</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">file</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">contents</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">getContents</span><span class="p">(),</span>
    <span class="n">startOffset</span> <span class="p">=</span> <span class="n">startLocation</span><span class="p">,</span>
    <span class="n">endOffset</span> <span class="p">=</span> <span class="n">endLocation</span> <span class="p">+</span> <span class="mi">1</span><span class="p">,</span>
<span class="p">)</span>

<span class="c1">// The `Location` to highlight, including the parentheses</span>
<span class="kd">val</span> <span class="py">problemLocation</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">file</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">contents</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">getContents</span><span class="p">(),</span>
    <span class="n">startOffset</span> <span class="p">=</span> <span class="n">startLocation</span> <span class="p">-</span> <span class="mi">1</span><span class="p">,</span>
    <span class="n">endOffset</span> <span class="p">=</span> <span class="n">endLocation</span> <span class="p">+</span> <span class="mi">2</span><span class="p">,</span>
<span class="p">)</span>

<span class="c1">// Construct the `LintFix` to put in today's date</span>
<span class="kd">val</span> <span class="py">dateFix</span> <span class="p">=</span> <span class="nc">LintFix</span><span class="p">.</span><span class="nf">create</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">name</span><span class="p">(</span><span class="s">"Update date"</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">dateLocation</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">with</span><span class="p">(</span><span class="nc">LocalDate</span><span class="p">.</span><span class="nf">now</span><span class="p">().</span><span class="nf">format</span><span class="p">(</span><span class="nc">DateTimeFormatter</span><span class="p">.</span><span class="nf">ofPattern</span><span class="p">(</span><span class="nc">DATE_PATTERN</span><span class="p">)))</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
    
<span class="c1">// Report the `Incident`</span>
<span class="kd">val</span> <span class="py">incident</span> <span class="p">=</span> <span class="nc">Incident</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">issue</span><span class="p">(</span><span class="nc">MISSING_OR_INVALID_DATE</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">location</span><span class="p">(</span><span class="n">problemLocation</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">message</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>   <span class="c1">// Whether the date is missing or invalid</span>
    <span class="p">.</span><span class="nf">fix</span><span class="p">(</span><span class="n">dateFix</span><span class="p">)</span>
<span class="n">context</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="n">incident</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="reporting-assignee-issues-️">Reporting assignee issues 🙋‍♀️</h3>

<p>Reporting assignee issues is largely similar to reporting date issues. We first check
if there is any assignee at all, i.e., if there is word attached to the <code class="language-plaintext highlighter-rouge">TODO</code> with a dash:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Capture everything after an optional dash</span>
<span class="c1">// until the first open parenthesis or whitespace</span>
<span class="kd">val</span> <span class="py">assigneePattern</span> <span class="p">=</span> <span class="s">""".*TODO-*(?&lt;$MATCH_KEY_ASSIGNEE&gt;[^:\(\s-]+).*\(.*\)"""</span><span class="p">.</span><span class="nf">toRegex</span><span class="p">()</span>
</code></pre></div></div>

<p>If there is no value in <code class="language-plaintext highlighter-rouge">MATCH_KEY_ASSIGNEE</code>, the <code class="language-plaintext highlighter-rouge">Incident</code> is reported and an auto-fix suggested:
<a href="https://imgur.com/cAUifq3"><img src="https://i.imgur.com/cAUifq3.png" width="400" /></a></p>

<p>Unlike the date auto-fix, however, we do not have any delimiters that would contain the assignee.
We thus need to do a bit of work to figure out what part of the current <code class="language-plaintext highlighter-rouge">TODO</code> should be replaced:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Find where the word "TODO" is inside the comment, and if there is a dash present</span>
<span class="c1">// We want to handle comments like "// TODO- " for example</span>
<span class="kd">var</span> <span class="py">nextCharIndex</span> <span class="p">=</span> <span class="n">commentText</span><span class="p">.</span><span class="nf">indexOf</span><span class="p">(</span><span class="s">"TODO"</span><span class="p">,</span> <span class="n">ignoreCase</span> <span class="p">=</span> <span class="k">true</span><span class="p">)</span> <span class="p">+</span> <span class="mi">4</span> <span class="c1">// length of "TODO</span>
<span class="k">if</span> <span class="p">(</span><span class="n">commentText</span><span class="p">[</span><span class="n">nextCharIndex</span><span class="p">]</span> <span class="p">==</span> <span class="sc">'-'</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">++</span><span class="n">nextCharIndex</span>
<span class="p">}</span>

<span class="c1">// Figure out the `Location` to be updated</span>
<span class="kd">val</span> <span class="py">commentStartOffset</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">getLocation</span><span class="p">(</span><span class="n">comment</span><span class="p">).</span><span class="n">start</span><span class="o">?.</span><span class="n">offset</span> <span class="o">?:</span> <span class="mi">0</span>
<span class="kd">val</span> <span class="py">endLocation</span> <span class="p">=</span> <span class="n">commentStartOffset</span> <span class="p">+</span> <span class="n">nextCharIndex</span>

<span class="kd">val</span> <span class="py">addAssigneeFix</span> <span class="p">=</span> <span class="nc">LintFix</span><span class="p">.</span><span class="nf">create</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">name</span><span class="p">(</span><span class="s">"Assign this TODO"</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="nc">Location</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
        <span class="n">file</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">contents</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">getContents</span><span class="p">(),</span>
        <span class="n">startOffset</span> <span class="p">=</span> <span class="n">commentStartOffset</span><span class="p">,</span>
        <span class="n">endOffset</span> <span class="p">=</span> <span class="n">endLocation</span>
    <span class="p">))</span>
    <span class="p">.</span><span class="nf">with</span><span class="p">(</span><span class="s">"// TODO-${getUserName()}"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">build</span><span class="p">()</span>
    
<span class="kd">val</span> <span class="py">incident</span> <span class="p">=</span> <span class="nc">Incident</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">issue</span><span class="p">(</span><span class="nc">MISSING_ASSIGNEE</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">location</span><span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="nf">getLocation</span><span class="p">(</span><span class="n">comment</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">message</span><span class="p">(</span><span class="s">"Missing assignee"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">fix</span><span class="p">(</span><span class="n">addAssigneeFix</span><span class="p">)</span>
<span class="n">context</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="n">incident</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="reporting-disordered-issues-">Reporting disordered issues 🤹</h3>

<p>The last type of error we want to fix are those that have all the elements in it, but are in the
incorrect order such as:</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TODO-Zarah: Some comments (2024-07-20)</span>

<span class="c1">// TODO-Zarah: (2024-07-20) Some comments</span>
</code></pre></div></div>

<p>In this scenario, constructing the <code class="language-plaintext highlighter-rouge">LintFix</code> is a bit more involved. We grab elements from the 
current comment and reconstruct them:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="py">replacementText</span> <span class="p">=</span> <span class="s">"// TODO"</span>

<span class="c1">// We are going to manipulate the existing comment text</span>
<span class="c1">// Drop anything before the word "TODO"</span>
<span class="c1">// There may or may not be a colon, so remove that separately</span>
<span class="kd">var</span> <span class="py">commentText</span> <span class="p">=</span> <span class="n">comment</span><span class="p">.</span><span class="n">text</span>
    <span class="p">.</span><span class="nf">substringAfter</span><span class="p">(</span><span class="s">"TODO"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">substringAfter</span><span class="p">(</span><span class="s">"todo"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">removePrefix</span><span class="p">(</span><span class="s">":"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">trimStart</span><span class="p">()</span>
</code></pre></div></div>

<p>Grab assignee value:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Find any assignee if available and re-use it
var currentAssignee = getUserName()

if (commentText.startsWith("-")) {
    val assigneeMatches = ASSIGNEE_CAPTURE_START_REGEX.find(commentText)
    if (assigneeMatches != null) {
        val assigneeMatchGroup = requireNotNull(assigneeMatches.groups[MATCH_KEY_ASSIGNEE])
        val assigneeRange = assigneeMatchGroup.range
        commentText = commentText.removeRange(assigneeRange).trimStart().removePrefix("-")
            .removePrefix(":").trimStart()
        currentAssignee = assigneeMatchGroup.value.trim()
    }
}
replacementText += "-$currentAssignee"
</code></pre></div></div>

<p>Grab the date:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// Find the string enclosed in parentheses
var dateReplacementValue = LocalDate.now().format(DateTimeFormatter.ofPattern(DATE_PATTERN))

val dateMatches = DATE_CAPTURE_REGEX.find(commentText)
if (dateMatches != null) {
    val dateMatchGroup = requireNotNull(dateMatches.groups[MATCH_KEY_DATE])
    commentText = commentText.removeRange(dateMatches.groups.first()!!.range).trimStart()
    dateReplacementValue = dateMatchGroup.value
}
replacementText += " ($dateReplacementValue)"
</code></pre></div></div>

<p>Follow the convention of adding a colon:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Add a colon if the remaining text does not have it yet</span>
<span class="k">if</span> <span class="p">(!</span><span class="n">commentText</span><span class="p">.</span><span class="nf">startsWith</span><span class="p">(</span><span class="s">":"</span><span class="p">))</span> <span class="p">{</span>
    <span class="n">replacementText</span> <span class="p">+=</span> <span class="s">": "</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And then do a direct replacement of the whole comment:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">fix</span> <span class="p">=</span> <span class="nc">LintFix</span><span class="p">.</span><span class="nf">create</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">name</span><span class="p">(</span><span class="n">message</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">text</span><span class="p">(</span><span class="n">comment</span><span class="p">.</span><span class="n">text</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>

<h3 id="testing-all-the-things-">Testing all the things 🧪</h3>

<p>There have been so many changes in this rule, which means a whole ton of new tests! I have added a whole
bunch of test scenarios to cover the different iterations I can think of.</p>

<p>The tests look mostly the same as before (see <a href="/2020/11/20/todo-test.html">this post</a> for reference), other than the addition of testing the <code class="language-plaintext highlighter-rouge">LintFix</code>es. Let’s take 
this <code class="language-plaintext highlighter-rouge">TODO</code> for example:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// TODO (2024-07-20): Some comments
</code></pre></div></div>
<p>Since we know what the comment should look like after our fix is applied, we can use that information
to construct our assertion:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.expectFixDiffs(
    """
        Fix for src/test/pkg/TestClass1.kt line 3: Assign this TODO:
        @@ -3 +3
        -     // TODO (2024-07-20): Some comments
        +     // TODO-$assignee (2024-07-20): Some comments
    """.trimIndent()
)
</code></pre></div></div>

<p>And that’s pretty much it!</p>

<p>All changes for both the <a href="https://github.com/zmdominguez/lint-rule-samples/blob/f51654a2e6f4697a58da9b4414d8c0d6eca87274/lint-checks/src/main/java/dev/zarah/lint/checks/TodoDetector.kt">detector</a> 
and the <a href="https://github.com/zmdominguez/lint-rule-samples/blob/f51654a2e6f4697a58da9b4414d8c0d6eca87274/lint-checks/src/test/java/dev/zarah/lint/checks/TodoDetectorTest.kt">tests</a>
are in GitHub.</p>

<hr />

<p>To read my past posts on this topic, check out the posts <a href="https://zarah.dev/tags/">tagged with Lint</a>, some
of which are linked below:</p>
<ul>
  <li><a href="/2020/11/18/todo-lint.html">Anatomy of a Lint issue</a></li>
  <li><a href="/2020/11/20/todo-test.html">Detectors Unit Testing</a></li>
  <li><a href="/2021/10/04/multi-module-lint.html">Multi-module rules</a></li>
  <li><a href="/2021/10/05/multi-module-lint-test.html">Multi-module Testing</a></li>
</ul>

<p>As always, here are some first-party resources for Lint:</p>
<ul>
  <li><a href="https://googlesamples.github.io/android-custom-lint-rules/api-guide.md.html">Lint API Guide</a></li>
  <li><a href="http://googlesamples.github.io/android-custom-lint-rules/user-guide.html">Lint User Guide</a></li>
  <li><a href="https://github.com/googlesamples/android-custom-lint-rules">Lint sample project</a></li>
  <li><a href="https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/">Framework rules</a></li>
</ul>

<hr />

<h3 id="issue-definitions">Issue definitions:</h3>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">const</span> <span class="kd">val</span> <span class="py">REQUIRED_FORMAT</span> <span class="p">=</span> <span class="s">"All TODOs must follow the format `TODO-Assignee (DATE_TODAY): Additional comments`"</span>
<span class="kd">val</span> <span class="py">IMPROPER_FORMAT</span><span class="p">:</span> <span class="nc">Issue</span> <span class="p">=</span> <span class="nc">Issue</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
    <span class="n">id</span> <span class="p">=</span> <span class="s">"ImproperTodoFormat"</span><span class="p">,</span>
    <span class="n">briefDescription</span> <span class="p">=</span> <span class="s">"TODO has improper format"</span><span class="p">,</span>
    <span class="n">explanation</span> <span class="p">=</span>
    <span class="s">"""
        $REQUIRED_FORMAT
        
        The assignee and the date are required information.
    """</span><span class="p">,</span>
    <span class="n">category</span> <span class="p">=</span> <span class="nc">Category</span><span class="p">.</span><span class="nc">CORRECTNESS</span><span class="p">,</span>
    <span class="n">priority</span> <span class="p">=</span> <span class="mi">3</span><span class="p">,</span>
    <span class="n">severity</span> <span class="p">=</span> <span class="nc">Severity</span><span class="p">.</span><span class="nc">ERROR</span><span class="p">,</span>
    <span class="n">implementation</span> <span class="p">=</span> <span class="nc">IMPLEMENTATION</span>
<span class="p">).</span><span class="nf">setAndroidSpecific</span><span class="p">(</span><span class="k">true</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">MISSING_ASSIGNEE</span><span class="p">:</span> <span class="nc">Issue</span> <span class="p">=</span> <span class="nc">Issue</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
    <span class="n">id</span> <span class="p">=</span> <span class="s">"MissingTodoAssignee"</span><span class="p">,</span>
    <span class="n">briefDescription</span> <span class="p">=</span> <span class="s">"TODO with no assignee"</span><span class="p">,</span>
    <span class="n">explanation</span> <span class="p">=</span>
    <span class="s">"""
        $REQUIRED_FORMAT
        
        Please put your name against this TODO. Assignees should be a camel-cased word, for example `ZarahDominguez`.
    """</span><span class="p">,</span>
    <span class="n">category</span> <span class="p">=</span> <span class="nc">Category</span><span class="p">.</span><span class="nc">CORRECTNESS</span><span class="p">,</span>
    <span class="n">priority</span> <span class="p">=</span> <span class="mi">3</span><span class="p">,</span>
    <span class="n">severity</span> <span class="p">=</span> <span class="nc">Severity</span><span class="p">.</span><span class="nc">ERROR</span><span class="p">,</span>
    <span class="n">implementation</span> <span class="p">=</span> <span class="nc">IMPLEMENTATION</span>
<span class="p">).</span><span class="nf">setAndroidSpecific</span><span class="p">(</span><span class="k">true</span><span class="p">)</span>
<span class="kd">val</span> <span class="py">MISSING_OR_INVALID_DATE</span><span class="p">:</span> <span class="nc">Issue</span> <span class="p">=</span> <span class="nc">Issue</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
    <span class="n">id</span> <span class="p">=</span> <span class="s">"MissingTodoDate"</span><span class="p">,</span>
    <span class="n">briefDescription</span> <span class="p">=</span> <span class="s">"TODO with no date"</span><span class="p">,</span>
    <span class="n">explanation</span> <span class="p">=</span>
    <span class="s">"""
        $REQUIRED_FORMAT
        
        Please put today's date in the yyyy-MM-dd format enclosed in parentheses, for example `(2024-07-20)`.
    """</span><span class="p">,</span>
    <span class="n">category</span> <span class="p">=</span> <span class="nc">Category</span><span class="p">.</span><span class="nc">CORRECTNESS</span><span class="p">,</span>
    <span class="n">priority</span> <span class="p">=</span> <span class="mi">3</span><span class="p">,</span>
    <span class="n">severity</span> <span class="p">=</span> <span class="nc">Severity</span><span class="p">.</span><span class="nc">ERROR</span><span class="p">,</span>
    <span class="n">implementation</span> <span class="p">=</span> <span class="nc">IMPLEMENTATION</span>
<span class="p">).</span><span class="nf">setAndroidSpecific</span><span class="p">(</span><span class="k">true</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="lint" /><summary type="html"><![CDATA[A few years ago, I wrote about writing a Lint rule to validate the format of TODO comments. Whilst I find that Lint is still difficult to grok, I have since learnt a little bit more that I feel a revisit of this rule is warranted.]]></summary></entry><entry><title type="html">I Dub Thee… Marginally better at RegEx</title><link href="https://zarah.dev/2024/07/21/regex-groups.html" rel="alternate" type="text/html" title="I Dub Thee… Marginally better at RegEx" /><published>2024-07-21T00:00:00+00:00</published><updated>2024-07-21T00:00:00+00:00</updated><id>https://zarah.dev/2024/07/21/regex-groups</id><content type="html" xml:base="https://zarah.dev/2024/07/21/regex-groups.html"><![CDATA[<p>Being a perpetual RegEx n00b, one thing I keep on forgetting is that it is easy to get tripped up when 
extracting information from an input.</p>

<p>I always forget that looking for a match does <em>not</em> really just give back just the matching values – 
they are instead contained in <code class="language-plaintext highlighter-rouge">Group</code>s.</p>

<h3 id="matches-and-groups-and-all-the-things-">Matches and Groups and all the things 💅</h3>

<p>For example, given the sentence “Welcome to zarah.dev!”, the value “zarah.dev” can be extracted by enclosing a 
pattern within parentheses:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">input</span> <span class="p">=</span> <span class="s">"Welcome to zarah.dev!"</span>

<span class="c1">// Capture everything (. = any character, * = multiple times) </span>
<span class="c1">// after the literal phrase "Welcome to " and before the literal exclamation mark</span>
<span class="kd">val</span> <span class="py">findPattern</span> <span class="p">=</span> <span class="s">"""Welcome to (.*)!"""</span><span class="p">.</span><span class="nf">toRegex</span><span class="p">()</span>

<span class="c1">// Find matches in the input (!! to simplify examples)</span>
<span class="kd">val</span> <span class="py">results</span> <span class="p">=</span> <span class="n">findPattern</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">input</span><span class="p">)</span><span class="o">!!</span>

</code></pre></div></div>

<p>We <em>know</em> that in this instance there is only one value that we care for – <code class="language-plaintext highlighter-rouge">zarah.dev</code>. But examining the 
contents of <code class="language-plaintext highlighter-rouge">results</code>, the returned <code class="language-plaintext highlighter-rouge">value</code> is actually the same as the input AND that there are two <code class="language-plaintext highlighter-rouge">Group</code>s
contained within this <code class="language-plaintext highlighter-rouge">MatchResult</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">println</span><span class="p">(</span><span class="s">"Result of find: ${results.value}"</span><span class="p">)</span> <span class="c1">// Result of find: Welcome to zarah.dev!</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Groups in match: ${results.groups.count()}"</span><span class="p">)</span> <span class="c1">// Groups in match: 2</span>
</code></pre></div></div>

<p>Looking into these further, we see that the <code class="language-plaintext highlighter-rouge">Group</code>:</p>
<ul>
  <li>at index 0 is the full input</li>
  <li>at index 1 is the value captured within the parentheses</li>
</ul>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">results</span><span class="p">.</span><span class="n">groups</span><span class="p">.</span><span class="nf">forEachIndexed</span> <span class="p">{</span> <span class="n">i</span><span class="p">,</span> <span class="n">group</span> <span class="p">-&gt;</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Group index $i, value is: ${group?.value}"</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// Group index 0, value is: Welcome to zarah.dev!</span>
<span class="c1">// Group index 1, value is: zarah.dev</span>
</code></pre></div></div>

<p>I was super confused by this at first, until I realised that OF COURSE it makes sense! The whole input is 
present as the first element because it <strong><em>DOES</em></strong> match the RegEx pattern that we have. 🙈</p>

<p>In simple enough cases like in this example, dealing with the indices is not too bad, we just need to keep in
mind that if we want to get value of anything after the “Welcome to “ phrase, we always need to look at the 
value of <code class="language-plaintext highlighter-rouge">group[1]</code>.</p>

<p>However, once we want to capture more and more patterns, it can get very confusing very quickly.</p>

<h3 id="gimme-all-the-groups-">Gimme All The Groups 🧮</h3>

<p>As a quick illustration, say the input is changed to something like:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">input</span> <span class="p">=</span> <span class="s">"Welcome to &lt;site&gt;! My name is &lt;owner&gt; and I talk about &lt;topic&gt;."</span>
</code></pre></div></div>
<p>and we want to retrieve the values of <code class="language-plaintext highlighter-rouge">site</code>, <code class="language-plaintext highlighter-rouge">owner</code>, and <code class="language-plaintext highlighter-rouge">topic</code>. For simplicity, we will assume that input
template always stays the same.</p>

<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">longInput</span> <span class="p">=</span> <span class="s">"Welcome to zarah.dev! My name is Zarah and I talk about Android."</span>
<span class="kd">val</span> <span class="py">sitePattern</span> <span class="p">=</span> <span class="s">"""Welcome to (.*)! My name is (.*) and I talk about (.*)\."""</span><span class="p">.</span><span class="nf">toRegex</span><span class="p">()</span>
</code></pre></div></div>

<p>Applying this pattern to the longer input:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">results</span> <span class="p">=</span> <span class="n">sitePattern</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">longInput</span><span class="p">)</span><span class="o">!!</span>
<span class="n">results</span><span class="p">.</span><span class="n">groups</span><span class="p">.</span><span class="nf">forEachIndexed</span> <span class="p">{</span> <span class="n">i</span><span class="p">,</span> <span class="n">group</span> <span class="p">-&gt;</span>
    <span class="nf">println</span><span class="p">(</span><span class="s">"Group index $i, value is: ${group?.value}"</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// Group index 0, value is: Welcome to zarah.dev! My name is Zarah and I talk about Android.</span>
<span class="c1">// Group index 1, value is: zarah.dev</span>
<span class="c1">// Group index 2, value is: Zarah</span>
<span class="c1">// Group index 3, value is: Android</span>
</code></pre></div></div>

<p>It is worth noting here that there is also a convenience method <a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-match-result/group-values.html"><code class="language-plaintext highlighter-rouge">groupValues</code></a>
available on <code class="language-plaintext highlighter-rouge">MatchResult</code> which will basically give the same information but within a <code class="language-plaintext highlighter-rouge">List</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">println</span><span class="p">(</span><span class="n">results</span><span class="p">.</span><span class="n">groupValues</span><span class="p">)</span>

<span class="c1">// [Welcome to zarah.dev! My name is Zarah and I talk about Android., zarah.dev, Zarah, Android]</span>
</code></pre></div></div>

<p>This is NOT to be confused with <em>another</em> convenience method that omits the zeroth <code class="language-plaintext highlighter-rouge">Group</code>:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">println</span><span class="p">(</span><span class="n">results</span><span class="p">.</span><span class="n">destructured</span><span class="p">.</span><span class="nf">toList</span><span class="p">())</span>

<span class="c1">// [zarah.dev, Zarah, Android]</span>
</code></pre></div></div>

<p>This is good enough if we only care about the values, but there are situations where we might want to also find
the location of each value inside the source string; such as when writing a Lint rule, for example.</p>

<h3 id="easier-regex-">Easier RegEx 🪪</h3>

<p>Up to this point we have been dealing with indices, but what I found easiest is referring to each extracted
value by name. And this is when <a href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/-match-named-group-collection/"><code class="language-plaintext highlighter-rouge">MatchNamedGroupCollection</code></a>
comes in to save the day!</p>

<p>From the documentation:</p>
<blockquote>
  <p>Extends <code class="language-plaintext highlighter-rouge">MatchGroupCollection</code> by introducing a way to get matched groups by name, when regex supports it.</p>
</blockquote>

<p>To recap, calling <code class="language-plaintext highlighter-rouge">find</code> on a <code class="language-plaintext highlighter-rouge">Regex</code> returns a <code class="language-plaintext highlighter-rouge">MatchResult</code>, which contains a <code class="language-plaintext highlighter-rouge">MatchGroupCollection</code>s.</p>

<p>To use <code class="language-plaintext highlighter-rouge">MatchNamedGroupCollection</code> instead, we need to give our capturing statement a name, with the syntax
being <code class="language-plaintext highlighter-rouge">?&lt;NAME&gt;</code>. Applying this to our example:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">namedSitePattern</span> <span class="p">=</span> <span class="s">"""Welcome to (?&lt;site&gt;.*)! My name is (?&lt;owner&gt;.*) and I talk about (?&lt;topic&gt;.*)\."""</span><span class="p">.</span><span class="nf">toRegex</span><span class="p">()</span>
</code></pre></div></div>

<p>To make it even easier to use, we can define these names in <code class="language-plaintext highlighter-rouge">val</code>s for easy reuse:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">KEY_SITE</span> <span class="p">=</span> <span class="s">"site"</span>
<span class="kd">val</span> <span class="py">KEY_OWNER</span> <span class="p">=</span> <span class="s">"owner"</span>
<span class="kd">val</span> <span class="py">KEY_TOPIC</span> <span class="p">=</span> <span class="s">"topic"</span>
<span class="kd">val</span> <span class="py">namedSitePattern</span> <span class="p">=</span> <span class="s">"""Welcome to (?&lt;$KEY_SITE&gt;.*)! My name is (?&lt;$KEY_OWNER&gt;.*) and I talk about (?&lt;$KEY_TOPIC&gt;.*)\."""</span><span class="p">.</span><span class="nf">toRegex</span><span class="p">()</span>
<span class="n">results</span> <span class="p">=</span> <span class="n">namedSitePattern</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">longInput</span><span class="p">)</span><span class="o">!!</span>
</code></pre></div></div>

<p>And then retrieve the individual <code class="language-plaintext highlighter-rouge">Group</code>s using their names:</p>
<div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">println</span><span class="p">(</span><span class="s">"Site: ${results.groups[KEY_SITE]?.value}"</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Owner: ${results.groups[KEY_OWNER]?.value}"</span><span class="p">)</span>
<span class="nf">println</span><span class="p">(</span><span class="s">"Topic: ${results.groups[KEY_TOPIC]?.value}"</span><span class="p">)</span>

<span class="c1">// Site: zarah.dev</span>
<span class="c1">// Owner: Zarah</span>
<span class="c1">// Topic: Android</span>
</code></pre></div></div>

<p>I learned about this when I was looking at improving the <a href="/2020/11/19/todo-detector.html">TODO Lint rule</a>
and it definitely made all the <code class="language-plaintext highlighter-rouge">String</code> manipulations much easier. Keen to see how TODO Lint Rule v2 looks like? 
Stay tuned! 📻</p>]]></content><author><name>Zarah Dominguez</name></author><category term="kotlin" /><category term="regex" /><summary type="html"><![CDATA[Being a perpetual RegEx n00b, one thing I keep on forgetting is that it is easy to get tripped up when extracting information from an input.]]></summary></entry><entry><title type="html">Extended ADB: En Vogue 💃</title><link href="https://zarah.dev/2024/02/02/adb-model.html" rel="alternate" type="text/html" title="Extended ADB: En Vogue 💃" /><published>2024-02-02T00:00:00+00:00</published><updated>2024-02-02T00:00:00+00:00</updated><id>https://zarah.dev/2024/02/02/adb-model</id><content type="html" xml:base="https://zarah.dev/2024/02/02/adb-model.html"><![CDATA[<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>&nbsp;</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>&nbsp;</td>
<td colspan="2"><code>$0</code></td>
<td>field variable containing the whole line</td>
</tr>
<tr>
<td>&nbsp;</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>&nbsp;</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>&nbsp;</td>
<td colspan="2"><code>NR</code></td>
<td>the record number (i.e. line number being read)</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2"><code>" - "</code></td>
<td>print a literal space, a dash, and a space</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2"><code>$1</code></td>
<td>field variable containing the first word</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2"><code>" ("</code></td>
<td>print a literal space and an open brace</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2"><code>substr($0, RSTART+6)</code></td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</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>&nbsp;</td>
<td>&nbsp;</td>
<td><code>$0</code></td>
<td>field variable that contains the whole line</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td><code>RSTART</code></td>
<td>the index of the last call to <code>match</code></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td><code>+6</code></td>
<td>move the pointer six places (basically skip "model:")&lt;/code&gt;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td><code>")"</code></td>
<td>&nbsp;</td>
<td>&nbsp;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>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="adb" /><summary type="html"><![CDATA[Last 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:]]></summary></entry><entry><title type="html">Extending an Interactive ADB 🔀</title><link href="https://zarah.dev/2023/09/21/adb-devices.html" rel="alternate" type="text/html" title="Extending an Interactive ADB 🔀" /><published>2023-09-21T00:00:00+00:00</published><updated>2023-09-21T00:00:00+00:00</updated><id>https://zarah.dev/2023/09/21/adb-devices</id><content type="html" xml:base="https://zarah.dev/2023/09/21/adb-devices.html"><![CDATA[<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> &lt;SERIAL_NUMBER&gt; <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> &lt;SERIAL_NUMBER&gt; 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">&amp;&amp;</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>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="adb" /><summary type="html"><![CDATA[A 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:]]></summary></entry><entry><title type="html">Making ADB a little bit dynamic 📱</title><link href="https://zarah.dev/2023/08/30/adb-deeplinks.html" rel="alternate" type="text/html" title="Making ADB a little bit dynamic 📱" /><published>2023-08-30T00:00:00+00:00</published><updated>2023-08-30T00:00:00+00:00</updated><id>https://zarah.dev/2023/08/30/adb-deeplinks</id><content type="html" xml:base="https://zarah.dev/2023/08/30/adb-deeplinks.html"><![CDATA[<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 &lt;SERIAL_NUMBER&gt;</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 &lt;SERIAL_NUMBER&gt;</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">&amp;&amp;</span> adb <span class="nt">-s</span> <span class="nv">$serial_number</span> reverse tcp:9000 tcp:9000 <span class="o">&amp;&amp;</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>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="deeplinks" /><category term="adb" /><summary type="html"><![CDATA[Android 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.]]></summary></entry><entry><title type="html">Bundling Things Nice and Pretty 💝</title><link href="https://zarah.dev/2023/08/21/bundle-parcel.html" rel="alternate" type="text/html" title="Bundling Things Nice and Pretty 💝" /><published>2023-08-21T00:00:00+00:00</published><updated>2023-08-21T00:00:00+00:00</updated><id>https://zarah.dev/2023/08/21/bundle-parcel</id><content type="html" xml:base="https://zarah.dev/2023/08/21/bundle-parcel.html"><![CDATA[<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">&lt;</span><span class="nc">DetailsExtras</span><span class="p">&gt;(</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>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="parcelize" /><summary type="html"><![CDATA[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 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.]]></summary></entry><entry><title type="html">Multi-module Lint Rules Follow Up: Suppressions ☠️</title><link href="https://zarah.dev/2022/02/15/deprecated-suppress.html" rel="alternate" type="text/html" title="Multi-module Lint Rules Follow Up: Suppressions ☠️" /><published>2022-02-15T00:00:00+00:00</published><updated>2022-02-15T00:00:00+00:00</updated><id>https://zarah.dev/2022/02/15/deprecated-suppress</id><content type="html" xml:base="https://zarah.dev/2022/02/15/deprecated-suppress.html"><![CDATA[<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">"""
            &lt;resources&gt;
                &lt;color name="some_colour"&gt;#d6163e&lt;/color&gt;
            &lt;/resources&gt;
        """</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">"""
            &lt;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"&gt;
                &lt;TextView android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textColor="@color/some_colour" 
                    tools:ignore="DeprecatedColorInXml" /&gt;
            &lt;/View&gt;
        """</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>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="lint" /><summary type="html"><![CDATA[It 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:]]></summary></entry><entry><title type="html">Debugging App Links in Android 12 🔗</title><link href="https://zarah.dev/2022/02/08/android12-deeplinks.html" rel="alternate" type="text/html" title="Debugging App Links in Android 12 🔗" /><published>2022-02-08T00:00:00+00:00</published><updated>2022-02-08T00:00:00+00:00</updated><id>https://zarah.dev/2022/02/08/android12-deeplinks</id><content type="html" xml:base="https://zarah.dev/2022/02/08/android12-deeplinks.html"><![CDATA[<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>
<figure class="align-center">
    <a href="https://imgur.com/4Kn6N5T"><img src="https://imgur.com/4Kn6N5T.gif" width="320" /></a><br />
    <figcaption>Deep linking into the product details screen</figcaption>
</figure>

<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>

<figure class="align-center">
    <a href="https://imgur.com/T9J8qI8"><img src="https://i.imgur.com/T9J8qI8.png" title="source: imgur.com" width="450" /></a><br />
    <figcaption>Successful linking!</figcaption>
</figure>

<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]&amp;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>
<figure class="align-center">
    <a href="https://i.imgur.com/OYEKHYO"><img src="https://i.imgur.com/OYEKHYO.png" title="Screenshot showing links" width="320" /></a><br />
    <figcaption>Manual permission for supported links</figcaption>
</figure>

<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 --&gt; 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 --&gt; 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 --&gt; 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>]]></content><author><name>Zarah Dominguez</name></author><category term="android" /><category term="deeplinks" /><summary type="html"><![CDATA[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 Android 12 behaviour changes:]]></summary></entry></feed>