cketti https://cketti.de/ Goodbye K-9 Mail <p><strong>TL;DR:</strong> I quit my job working on <a href="https://www.thunderbird.net/mobile/">Thunderbird for Android</a> and <a href="https://k9mail.app/">K-9 Mail</a> at MZLA.</p> <p>My personal journey with K-9 Mail started in late 2009, shortly after getting my first Android device<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. The pre-installed Email app didn’t work very well with my email provider. When looking for alternatives, I discovered K-9 Mail. It had many of the same issues<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>. But it was an active open source project that accepted contributions. I started fixing the problems I was experiencing and contributed these changes to K-9 Mail. It was a very pleasant experience and so I started fixing bugs reported by other users.</p> <p>In February 2010, <a href="https://metasocial.com/@jesse">Jesse Vincent</a>, the founder of the K-9 Mail project, offered me commit access to the Subversion<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> repository. According to my email archive, I replied with the following text:</p> <blockquote> <p>Thank you! I really enjoyed writing patches for K-9 and gladly accept your offer. But I probably won’t be able to devote as much time to the project as I do right now for a very long time. I hope that’s not a big problem.</p> </blockquote> <p>My prediction turned out to be not quite accurate. I was able to spend a lot of time working on K-9 Mail and quickly became one of the most active contributors.</p> <p>In 2012, Jesse hired me to work on <a href="https://kaitenmail.com/">Kaiten Mail</a>, a commercial closed-source fork of K-9 Mail. The only real differences between the apps were moderate changes to the user interface. So most of the features and bug fixes we created for Kaiten Mail also went into K-9 Mail. This was important to me and one of the reasons I took the job.</p> <p>In early 2014, Jesse made me the <a href="https://groups.google.com/g/k-9-mail/c/6OzXyOoksf8/m/xrbvkd1xO8AJ">K-9 Mail project leader</a><sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>. With Kaiten Mail, end-user support was eating up a lot of time and eventually motivation to work on the app. So we stopped working on it around the same time and the app slowly faded away.</p> <p>To pay the bills, I started working as a freelancing Android developer<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup>. Maybe not surprisingly, more often than not I was contracted to work on email clients. Whenever I was working on a closed source fork of K-9 Mail<sup id="fnref:6" role="doc-noteref"><a href="#fn:6" class="footnote" rel="footnote">6</a></sup>, I had a discounted hourly rate that would apply when working on things that were contributed to K-9 Mail. This was mostly bug fixes, but also the odd feature every now and then.</p> <p>After a contract ended in 2019, I decided to apply for a grant from the <a href="https://prototypefund.de/">Prototype Fund</a> to work on adding JMAP support to K-9 Mail<sup id="fnref:7" role="doc-noteref"><a href="#fn:7" class="footnote" rel="footnote">7</a></sup>. This allowed me to basically work full-time on the project. When the funding period ended in 2020, the COVID-19 pandemic was in full swing. At that time I didn’t feel like looking for a new contract. I filled my days working on K-9 Mail to mute the feeling of despair about the world. I summarized my 2020 in the blog post <a href="https://cketti.de/2021/01/14/my-first-year-as-a-full-time-open-source-developer/">My first year as a full-time open source developer</a>.</p> <p>Eventually I had to figure out how to finance this full-time open source developer lifestyle. I ended up <a href="https://k9mail.app/2021/02/14/K-9-Mail-is-looking-for-funding">asking</a> K-9 Mail users to donate so I could be paid to dedicate 80% of my time to work on the app. This worked out quite nicely and I wrote about it here: <a href="https://k9mail.app/2022/01/18/2021-in-Review">2021 in Review</a>.</p> <p>I first learned about plans to create a Thunderbird version for Android in late 2019. I was approached because one of the options considered was basing Thunderbird for Android on K-9 Mail. At the time, I wasn’t really interested in working on Thunderbird for Android. But I was more than happy to help turn the K-9 Mail code base into something that Thunderbird could use as a base for their own app. However, it seemed the times where we had availability to work on such a project never aligned. And so nothing concrete happened. But we stayed in contact.</p> <p>In December 2021, it seemed to have become a priority to find a solution for the many Thunderbird users asking for an Android app. By that time, I had realized that funding an open source project via donations requires an ongoing fundraising effort. Thunderbird was already doing this for quite some time and getting pretty good at it. I, on the other hand, was not looking forward to the idea of getting better at fundraising.<br /> So, when I was asked again whether I was interested in K-9 Mail and myself joining the Thunderbird project, I said yes. It took another six months for us to figure out the details and <a href="https://k9mail.app/2022/06/13/K-9-Mail-and-Thunderbird">announce</a> the news to the public.</p> <p>Once under the Thunderbird umbrella, we worked<sup id="fnref:8" role="doc-noteref"><a href="#fn:8" class="footnote" rel="footnote">8</a></sup> on adding features to K-9 Mail that we wanted an initial version of Thunderbird for Android to have. The mobile team slowly grew to include another <a href="https://blog.thunderbird.net/2023/03/thunderbird-for-android-k-9-mail-february-progress-report/">Android developer</a>, then a <a href="https://blog.thunderbird.net/2024/07/thunderbird-for-android-k-9-mail-june-2024-progress-report/">manager</a>. While organizationally the design team was its own group, there was always at least one designer available to work with the mobile team on the Android app. And then there were a bunch of other teams to do the things for which you don’t need Android engineers: support, communication, donations, etc.</p> <p>In October 2024, we finally released the <a href="https://blog.thunderbird.net/2024/10/thunderbird-for-android-8-0-takes-flight/">first version of Thunderbird for Android</a>. The months leading up to the release were quite stressful for me. All of us were working on many things at the same time to not let the targeted release date slip too much. We never worked overtime, though. And we got additional paid time off after the release ❤️</p> <p>After a long vacation, we started 2025 with a more comfortable pace. However, the usual joy I felt when working on the app, didn’t return. I finally realized this at the beginning of February, while being sick in bed and having nothing better to do than contemplating life.<br /> I don’t think I was close to a burnout – work wasn’t that much fun anymore, but it was far from being unbearable. I’ve been there before. And in the past it never was a problem to step away from K-9 Mail for a few months. However, it’s different when it’s your job. But since I am in the very fortunate position of being able to afford taking a couple of months off, I decided to do just that. So the question was whether to take a <a href="https://en.wikipedia.org/wiki/Sabbatical">sabbatical</a> or to quit.<br /> Realistically, permanently walking away from K-9 Mail never was an option in the past. There was no one else to take over as a maintainer. It would have most likely meant the end of the project. K-9 Mail was always too important to me to let that happen.<br /> But this is no longer an issue. There’s now a whole team behind the project and me stepping away no longer is an existential threat to the app.</p> <p>I want to explore what it feels like to do something else without going back to the project being a foregone conclusion. That is why I quit my job at MZLA.</p> <p>It was a great job and I had awesome coworkers. I can totally recommend <a href="https://www.mozilla.org/en-US/careers/listings/?team=MZLA/Thunderbird">working</a> with these people and will miss doing so 😢</p> <hr /> <p>I have no idea what I’ll end up doing next. A coworker asked me whether I’ll stick to writing software or do something else entirely. I was quite surprised by this question. Both because in hindsight it felt like an obvious question to ask and because I’ve never even considered doing something else. I guess that means I’m very much still a software person and will be for the foreseeable future.</p> <p>During my vacation I very much enjoyed being a beginner and learning about technology I haven’t worked with as a developer before (NFC smartcards, USB HID, Bluetooth LE). So I will probably start a lot of personal projects and finish few to none of them 😃</p> <p>I think there’s a good chance that – after an appropriately sized break – I will return as a volunteer contributor to K-9 Mail/Thunderbird for Android.</p> <p>But for now, I say: <strong>Goodbye K-9 Mail</strong> 👋</p> <hr /> <p>This leaves me with saying thank you to everyone who contributed to K-9 Mail and Thunderbird for Android over the years. People wrote code, translated the app, reported bugs, helped other users, gave money, promoted the app, and much more. Thank you all 🙏</p> <hr /> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:1" role="doc-endnote"> <p>My youngest brother gave me his “old” <a href="https://en.wikipedia.org/wiki/HTC_Magic">HTC Magic</a> ❤️ <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>This is not surprising when you learn that <a href="https://obra.livejournal.com/96524.html">K-9 Mail was forked from the AOSP Email app in October 2008</a>. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:3" role="doc-endnote"> <p>The K-9 Mail project was initially hosted on <a href="https://code.google.com/archive/p/k9mail/">Google Code</a> where Git wasn’t available at the time. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:4" role="doc-endnote"> <p>Jesse moved on to <a href="https://keyboard.io">build keyboards</a>. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:5" role="doc-endnote"> <p>As a freelancer I rarely worked more than 30 hours a week. I made sure there was always enough time to work on K-9 Mail. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:6" role="doc-endnote"> <p>There’s surprisingly many of these forks around. Yet hardly any of the teams working on them ever contributed back to K-9 Mail 😞 <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:7" role="doc-endnote"> <p>The app currently doesn’t support JMAP. However, the <a href="https://github.com/thunderbird/thunderbird-android/tree/be2af5c6a0bce08385fc3f654c1185ccf9db3859/backend/jmap">code</a> for basic JMAP support can be found in the repository. <a href="#fnref:7" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:8" role="doc-endnote"> <p>You can find monthly progress reports about this in the <a href="https://blog.thunderbird.net/category/mobile-news/">mobile section of the Thunderbird blog</a>. <a href="#fnref:8" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div> Wed, 26 Feb 2025 00:00:00 +0100 https://cketti.de/2025/02/26/goodbye-k9mail/ https://cketti.de/2025/02/26/goodbye-k9mail/ My second year as a full-time open source developer <p>2020 was <a href="https://cketti.de/2021/01/14/my-first-year-as-a-full-time-open-source-developer/">my first year as a full-time open source developer</a>. Writing about it in this blog was a nice way to reflect on the year. And some people seemed to have enjoyed it.</p> <p>So here’s a summary of how my 2021 went.</p> <h2 id="k-9-mail">K-9 Mail</h2> <p>Most of my “open source time” is spent on <a href="https://github.com/k9mail/k-9">K-9 Mail</a>. I first started contributing to the project in 2010 and became the project lead in 2014.<br /> The reason I decided to dedicate all of my time to open source is because I wanted to spend more time working on K-9 Mail. There’s certainly enough issues in the bug tracker to keep me busy for a couple of years.</p> <p>The goal for 2020 was to release a new version of the app (the last stable version was from September 2018). If you’ve read my blog post reviewing that year, you know that I didn’t manage to accomplish that goal. But I’m happy to report that 2021 was the year that finally saw the release of a new stable version - <a href="https://k9mail.app/2021/07/24/K-9-Mail-is-back">K-9 Mail 5.800</a>.</p> <p>Even though it took a very long time to get that version done, it was far from perfect. When we built up the new user interface, not all features of the old UI made it into the new version right away. It was always the goal to re-implement those features. But development dragged on. So at some point I decided it was more important to release a new version than it was to get to feature parity.<br /> Of course, not all users agreed. Some hated the new user interface. Some really missed the features that didn’t make the cut. Some shared their opinions on how to run an open source project 🙄 But there were also a lot of users who liked the new look.</p> <p><a href="https://github.com/k9mail/k-9/milestone/28">Work</a> on the <a href="https://forum.k9mail.app/t/plans-for-k-9-mail-6-000/2936">next release</a> is ongoing, but somewhat slow. It doesn’t help that some device vendors decided their Android version will do things differently for no good reason 😭</p> <p>Read more on what the project has been up to in <a href="https://k9mail.app/2022/01/18/2021-in-Review">2021 in Review</a>.</p> <h2 id="blog-posts">Blog posts</h2> <p>cketti from last year believed he was getting better at writing blog posts. It looks like his assessment has not been entirely accurate.</p> <p>The only post I published in 2021 was a list of things I did the year before that 😐</p> <ul> <li><a href="https://cketti.de/2021/01/14/my-first-year-as-a-full-time-open-source-developer/">My first year as a full-time open source developer</a></li> </ul> <h2 id="contributions-to-open-source-projects">Contributions to open source projects</h2> <p>Even though I spent almost all of my time working on K-9 Mail, I still managed to make a couple of small contributions to other open source projects.</p> <h3 id="own-projects">Own projects</h3> <ul> <li><a href="https://github.com/cketti/AdbScreenAwake">AdbScreenAwake</a> - Inspired by <a href="https://twitter.com/ErikHellman">@ErikHellman</a> complaining that there should be an Android developer option called “Keep screen on while ADB is connected”, I hacked up a quick prototype app to do just that. You can read a bit more about it in this <a href="https://twitter.com/cketti/status/1454129384017866755">Twitter thread</a>.</li> <li><a href="https://github.com/cketti/jitsi-hacks">Jitsi Hacks</a> - I updated some of the hacks to make them work with newer versions of Jitsi Meet. (Un)fortunately, the Jitsi team is evolving Jitsi Meet at an incredible pace. Whenever they change the user interface, there’s a good chance some of the hacks will stop working 😢</li> <li><a href="https://github.com/cketti/MailToCompat">MailToCompat</a> - I deprecated the library because the functionality is now available as <a href="https://developer.android.com/reference/kotlin/androidx/core/net/MailTo">androidx.core.net.MailTo</a>. Check out the blog post <a href="https://cketti.de/2020/06/22/android-net-mailto-is-broken/">android.net.MailTo is broken</a> for more details.</li> <li><a href="https://github.com/cketti/thegreatsuspender">The Great Suspender (fork)</a> - After the extension in the Chrome Web Store was suspected to contain malware, I forked the repository to build my own version with all of the “phone home” functionality removed.<br /> Please note that I have no intention of maintaining this extension for anyone but myself. Feel free to use it. But don’t tell me about it if it doesn’t work for you 😜</li> </ul> <h3 id="other-projects">Other projects</h3> <ul> <li><a href="https://github.com/AndroidStudyGroup/conferences/pulls?q=is%3Apr+author%3Acketti+created%3A2021">AndroidStudyGroup/conferences</a> - added a few events; but as one of the maintainers, I mostly click the “Merge pull request” button</li> <li><a href="https://github.com/android/storage-samples/pulls?q=is%3Apr+created%3A2021+author%3Acketti">android/storage-samples</a> - various bug fixes</li> <li><a href="https://github.com/c-base/c-base-org-lektor">c-base/c-base-org-lektor</a> - <a href="https://github.com/uwekamper">uwekamper</a> and I rewrote the <a href="https://c-base.org/">c-base website</a></li> <li><a href="https://github.com/vector-im/element-android/pull/3965">Element Android</a> - fixed the preview when sharing images</li> <li><a href="https://github.com/ErikHellman/base45-kotlin/pull/1">ErikHellman/base45-kotlin</a> - small improvements</li> <li><a href="https://github.com/EventFahrplan/EventFahrplan/pulls?q=is%3Apr+created%3A2021+commenter%3Acketti">EventFahrplan</a> - removed unused code and reviewed a couple of pull requests</li> <li><a href="https://github.com/mikepenz/FastAdapter/pull/985">FastAdapter</a> - added a ‘reorder via drag &amp; drop’ screen to the sample app; made small changes to the library to support this use case</li> <li><a href="https://github.com/JakeWharton/jakewharton.com/pull/43">jakewharton.com</a> - I create pull requests to fix typos in blog posts if the site is built from a Git repository 😀</li> <li><a href="https://github.com/mikepenz/MaterialDrawer/pull/2716">MaterialDrawer</a> - small fix to support loading images via URIs using a custom scheme</li> <li><a href="https://gitlab.com/signald/signald/-/merge_requests?scope=all&amp;state=all&amp;author_username=cketti">signald</a> - small changes and fixes</li> <li><a href="https://gitlab.com/signald/signald.org/-/merge_requests?scope=all&amp;state=all&amp;author_username=cketti">signald.org</a> - fixed some links</li> <li><a href="https://github.com/rignaneseleo/SlimSocial-for-Twitter/pull/20">SlimSocial for Twitter</a> - fixed a bug in the code handling the Share intent</li> </ul> <h2 id="donations-to-open-source-projects">Donations to open source projects</h2> <p>Aside from recurring donations set up the previous year, 2021 was the year of spontaneous donations. I mostly sponsored projects and individuals because I liked their work, not because I was using the software or service they had created.</p> <ul> <li><a href="https://dino.im/">Dino</a></li> <li><a href="https://fosstodon.org/">Fosstodon</a></li> <li><a href="https://gitea.io/">gitea</a></li> <li><a href="https://keepassxc.org/">KeePassXC</a></li> <li><a href="https://letsencrypt.org/">Let’s Encrypt</a></li> <li><a href="https://liberapay.com/">Liberapay</a></li> <li><a href="https://matrix.org/">Matrix</a></li> <li><a href="https://pixelfed.org/">pixelfed</a></li> <li><a href="https://simplysecure.org/">Simply Secure</a></li> <li><a href="https://www.torproject.org/">The Tor Project</a></li> <li><a href="https://www.thunderbird.net/">Thunderbird</a></li> <li><a href="https://tusky.app/">Tusky</a></li> <li><a href="https://webrtc.rs/">WebRTC.rs</a></li> </ul> <p>The donations to The Tor Project, WebRTC.rs, pixelfed, Simply Secure, and gitea I made via <a href="https://fundoss.org/share/660b36a26c">FundOSS</a>.</p> <p>I also sponsored the following individuals:</p> <ul> <li><a href="https://www.bleeptrack.de/">bleeptrack</a> - creates art using code</li> <li><a href="https://github.com/chrisrhymes">chrisrhymes</a> - creator of the <a href="https://github.com/chrisrhymes/bulma-clean-theme">bulma-clean-theme</a> Jekyll theme I used for the Jitsi Hacks website</li> <li><a href="https://github.com/johnjohndoe">johnjohndoe</a> - maintainer of the <a href="https://github.com/EventFahrplan/EventFahrplan">EventFahrplan</a> Android app</li> <li><a href="https://github.com/Mariatta">Mariatta</a> - does lots of things in the Python community; gave an inspiring talk at the <a href="https://globalmaintainersummit.github.com/">Global Maintainer Summit 2021</a></li> <li><a href="https://github.com/mikepenz">mikepenz</a> - creator of the <a href="https://github.com/mikepenz/MaterialDrawer">MaterialDrawer</a> and <a href="https://github.com/mikepenz/FastAdapter">FastAdapter</a> libraries we use in K-9 Mail</li> <li><a href="https://liberapay.com/s.pantaleev/">Slavi Pantaleev</a> - creator of <a href="https://github.com/spantaleev/matrix-docker-ansible-deploy">matrix-docker-ansible-deploy</a>, which I use to manage my Matrix server</li> </ul> <h2 id="financials">Financials</h2> <p>In 2020 my main source of income was a grant from the <a href="https://prototypefund.de/en/">Prototype Fund</a>. But applying for grants and the associated paperwork is… not fun. So in 2021 I tried something else.</p> <h3 id="k-9-mail-1">K-9 Mail</h3> <p>On <a href="https://fsfe.org/activities/ilovefs/">I love Free Software Day</a> I published the blog post <a href="https://k9mail.app/2021/02/14/K-9-Mail-is-looking-for-funding">K-9 Mail is looking for funding</a>. I was asking users for money so I could dedicate 80% of my time to working on the app.</p> <p>The post was picked up by various news sites and in February alone K-9 Mail’s Liberapay account received around 19,000 EUR 🎉</p> <p>At the end of the year a total of <strong>51,002.55 EUR</strong> had been donated via Liberapay, GitHub Sponsors and bank transfers. Check out <a href="https://k9mail.app/2022/01/07/Donations-in-2021">Donations in 2021</a> for more details.</p> <h3 id="liberapay">Liberapay</h3> <p>In 2021 my <a href="https://liberapay.com/cketti">personal Liberapay account</a> received <strong>58.46 EUR</strong> from 5 patrons 💖</p> <h3 id="total">Total</h3> <p>The donations for K-9 Mail together with the donations to my personal Liberapay account add up to <strong>51,061.01 EUR</strong>.</p> <h2 id="reflections">Reflections</h2> <p>If it was a regular job, a salary of €51k would be very disappointing to me. But it is not. I get to do what I love every day. And on days where I don’t feel the love, I just do something else.<br /> It’s great to see that what I create is useful to others. It’s awesome that so many people find this work valuable enough to give money 💖</p> <p>My plan was to use 80% of my time to work on K-9 Mail. But in 2021 it was much more than that. It hasn’t been a problem so far. But I think I need to find a healthier balance.</p> <p>I feel relieved that I finally managed to release a new stable version of K-9 Mail. Hopefully it won’t be another 3 years until the next release 🤞</p> <h2 id="plans-for-2022">Plans for 2022</h2> <p>The goals for 2022 are simple. Keep working on K-9 Mail to make the app better. And convince enough people to sponsor my open source work, so I can keep doing this full time.</p> <p>If you want to help, <a href="https://cketti.de/sponsor/">become a sponsor</a> 😃</p> <h2 id="thanks">Thanks</h2> <p>None of this would have been possible without the people donating to the K-9 Mail project and the ones sponsoring me directly. Thank you! ❤️</p> Sat, 22 Jan 2022 00:00:00 +0100 https://cketti.de/2022/01/22/my-second-year-as-a-full-time-open-source-developer/ https://cketti.de/2022/01/22/my-second-year-as-a-full-time-open-source-developer/ My first year as a full-time open source developer <p>Pandemic aside, 2020 has been an exciting year for me.</p> <p>For more than 10 years I was working on open source software in my spare time. Then, in late 2019, a grant from the <a href="https://prototypefund.de/">Prototype Fund</a> allowed me to work on <a href="https://k9mail.app/">K-9 Mail</a> full-time for six months. And it was awesome! So afterwards I decided to just keep going. I was working as a freelancer, so there wasn’t really a job I had to quit first.</p> <p>Here’s a probably incomplete list of open source-y things I did in 2020. It’s also rather brief because I still haven’t figured out how to write blog posts without rewriting most of it, then rewriting it some more, and then throwing away half of it because I don’t like those parts anymore.<br /> It feels like I’m slowly getting better, though 🤞</p> <h2 id="k-9-mail">K-9 Mail</h2> <p>I’ve spent most of my time working on K-9 Mail. The app was in a <a href="https://k9mail.app/2020/06/01/Whats-Up-With-K-9-Mail">difficult state</a>. The last stable release was in September 2018. My goal was to publish a new stable version in 2020. By releasing 22 beta versions we got closer to that goal. But the app is still not in a state where I’m comfortable cutting a new stable release 😞</p> <p>As K-9 Mail’s maintainer I’m also dealing with all the non-development-related aspects of the project:</p> <ul> <li>In 2020 the website got a new domain name – <a href="https://k9mail.app">k9mail.app</a> – and a redesign, thanks to <a href="https://github.com/AnXh3L0">Anxhelo Lushka</a>.</li> <li>I set up a <a href="https://www.discourse.org/">discourse</a> instance to replace the mailing list: <a href="https://forum.k9mail.app/">forum.k9mail.app</a></li> <li>And the project now has a presence on Twitter (<a href="https://twitter.com/k9mail_app">@k9mail_app</a>) and Mastodon (<a href="https://fosstodon.org/@k9mail">@[email protected]</a>).</li> </ul> <h2 id="jitsi-hacks">Jitsi Hacks</h2> <p>Due to the pandemic much of my social life took place in video conferences, powered by the wonderful open source project <a href="https://jitsi.org/">Jitsi</a>. At some point I started poking around in the browser’s developer tools trying to do things the web interface wasn’t built to do. This resulted in multiple “hacks” that I shared with friends.</p> <p>Since by then I had convinced myself that I was an “open source developer” I figured I should publish this little side project. So I created a small website containing “installation” and usage instructions for the different “hacks”.<br /> I am quite proud of myself because I refrained from buying a domain for the project. A sub-domain does just fine for a small side project: <a href="https://jitsi-hacks.cketti.eu/">jitsi-hacks.cketti.eu</a></p> <h2 id="blog-posts">Blog posts</h2> <p>In 2020 I published five blog posts. That is five more than the year prior 😀</p> <ul> <li><a href="https://cketti.de/2020/05/23/content-uris-and-okhttp/">content:// URIs and OkHttp</a></li> <li><a href="https://k9mail.app/2020/06/01/Whats-Up-With-K-9-Mail">What’s up with K-9 Mail?</a></li> <li><a href="https://cketti.de/2020/06/22/android-net-mailto-is-broken/">android.net.MailTo is broken</a></li> <li><a href="https://cketti.de/2020/07/07/why-i-hide-github-issue-comments/">Why I hide GitHub issue comments</a></li> <li><a href="https://cketti.de/2020/09/03/avoid-intent-resolveactivity/">Avoid Intent.resolveActivity()</a></li> </ul> <h2 id="contributions-to-open-source-projects">Contributions to open source projects</h2> <p>In 2020 I contributed to quite the variety of open source projects.</p> <h3 id="own-projects">Own projects</h3> <p>Aside from the ones previously mentioned I also worked on these projects of mine:</p> <ul> <li><a href="https://github.com/cketti/MailToCompat">MailToCompat</a> - because Android’s <code class="language-plaintext highlighter-rouge">MailTo</code> class is <a href="https://cketti.de/2020/06/22/android-net-mailto-is-broken/">broken</a> I created a fixed version</li> <li><a href="https://github.com/cketti/SafeContentResolver">SafeContentResolver</a> - after existing for 4 years as version 0.9.0 without any issues I figured it was time to promote the library to version 1.0.0</li> <li><a href="https://github.com/cketti/smartwatch2048">smartwatch2048</a> - open sourced an old and abandoned project</li> </ul> <h3 id="other-projects">Other projects</h3> <ul> <li><a href="https://github.com/android/storage-samples/pull/55">android/storage-samples</a> - corrected some inaccuracies in code/comments related to Android’s URI permissions 🧐</li> <li><a href="https://github.com/AndroidStudyGroup/conferences/pulls?q=is%3Apr+author%3Acketti+created%3A2020">AndroidStudyGroup/conferences</a> - added some conferences, improved the CI workflow, and cleaned up the generated HTML/JavaScript</li> <li><a href="https://github.com/androidx/androidx/commits?author=cketti&amp;since=2020-01-01&amp;until=2020-12-31">androidx</a> - fixed a memory leak, fixed the <code class="language-plaintext highlighter-rouge">MailTo</code> class, fixed <code class="language-plaintext highlighter-rouge">ShareCompat.IntentBuilder</code> 🐛</li> <li><a href="https://github.com/daattali/beautiful-jekyll/pulls?q=is%3Apr+author%3Acketti+created%3A2020">beautiful-jekyll</a> - various minor improvements I wanted to use on cketti.de</li> <li><a href="https://github.com/chrisrhymes/bulma-clean-theme/pull/75">bulma-clean-theme</a> - very minor fix to the generated HTML</li> <li><a href="https://github.com/c-base/c-calendar/pulls?q=is%3Apr+author%3Acketti+created%3A2020">c-calendar</a> - improved the <a href="https://www.c-base.org/calendar">c-base calendar</a> page</li> <li><a href="https://github.com/dbbahnhoflive/dbbahnhoflive-android/pull/3">dbbahnhoflive-android</a> - some minor fixes; apparently the first community contribution 🥇</li> <li><a href="https://github.com/discourse/discourse/pull/9740">discourse</a> - fixed a bug in the quick start guide I noticed when setting up K-9 Mail’s new forum</li> <li><a href="https://github.com/EventFahrplan/EventFahrplan/pulls?q=is%3Apr+author%3Acketti+created%3A2020">EventFahrplan</a> - various fixes and refactorings</li> <li><a href="https://github.com/M66B/FairEmail/pull/187">FairEmail</a> - replaced usage of the <code class="language-plaintext highlighter-rouge">MailTo</code> class with the one from the AndroidX Core library</li> <li><a href="https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/merge_requests/1317">gstreamer/gst-plugins-bad</a> - make <code class="language-plaintext highlighter-rouge">curlsmtpsink</code> use a less wrong date format when generating emails</li> <li><a href="https://github.com/iNPUTmice/jmap/pulls?q=is%3Apr+author%3Acketti+created%3A2020">iNPUTmice/jmap</a> - various fixes and small improvements</li> <li><a href="https://github.com/JakeWharton/jakewharton.com/pull/23">jakewharton.com</a> - fixed number format in a blog post; apparently the first real external contribution 🎉</li> <li><a href="https://github.com/jitsi/handbook/pull/121">jitsi/handbook</a> - small improvements to the documentation on how to build Jitsi Meet from source</li> <li><a href="https://github.com/JetBrains/kotlin/pulls?q=is%3Apr+author%3Acketti+created%3A2020">kotlin</a> - fixes to the parameter popup; <a href="https://twitter.com/cketti/status/1303019601547145216">tweet including video</a> 🎥</li> <li><a href="https://github.com/square/leakcanary/pull/1806">leakcanary</a> - make it harder to accidentally include LeakCanary in release builds</li> <li><a href="https://github.com/slackhq/moshi-gson-interop/pulls?q=is%3Apr+author%3Acketti+created%3A2020">moshi-gson-interop</a> - small documentation fixes</li> <li><a href="https://github.com/square/okhttp/pull/6164">okhttp</a> - fixed code to read length of <a href="https://en.wikipedia.org/wiki/X.690#DER_encoding">DER-encoded</a> values</li> <li><a href="https://github.com/splitwise/TokenAutoComplete/pulls?q=is%3Apr+author%3Acketti+created%3A2020">TokenAutoComplete</a> - small fixes/improvements needed for K-9 Mail</li> </ul> <h2 id="donations-to-open-source-projects">Donations to open source projects</h2> <p>I usually use the <a href="https://fsfe.org/activities/ilovefs/2020/index.en.html">I love Free Software Day</a> as a reminder to financially support the open source projects that are important to me. In 2020 I gave money to the following projects:</p> <ul> <li><a href="https://github.com/andOTP/andOTP">andOTP</a></li> <li><a href="https://dino.im/">Dino</a></li> <li><a href="https://fosstodon.org/">Fosstodon</a></li> <li><a href="https://keepassxc.org/">KeePassXC</a></li> <li><a href="https://letsencrypt.org/">Let’s Encrypt</a></li> <li><a href="https://liberapay.com/">Liberapay</a></li> <li><a href="https://lichess.org/">lichess.org</a></li> <li><a href="https://matrix.org/">Matrix</a></li> <li><a href="https://signal.org/">Signal</a></li> <li><em>The Great Suspender</em> - Sadly, the software seems to have changed owners since then. And the extension <a href="https://www.theregister.com/2021/01/07/great_suspender_malware/">might contain malware now</a> 😞</li> <li><a href="https://www.thunderbird.net/">Thunderbird</a></li> <li><a href="https://tusky.app/">Tusky</a></li> <li><a href="https://ubuntu.com/">Ubuntu</a></li> </ul> <p>I also sponsored the following individuals via GitHub sponsors:</p> <ul> <li><a href="https://github.com/chrisrhymes">chrisrhymes</a> - creator of the <a href="https://github.com/chrisrhymes/bulma-clean-theme">bulma-clean-theme</a> Jekyll theme I used for the Jitsi Hacks website</li> <li><a href="https://github.com/johnjohndoe">johnjohndoe</a> - maintainer of the <a href="https://github.com/EventFahrplan/EventFahrplan">EventFahrplan</a> Android app</li> <li><a href="https://github.com/mikepenz">mikepenz</a> - creator of the <a href="https://github.com/mikepenz/MaterialDrawer">MaterialDrawer</a> and <a href="https://github.com/mikepenz/FastAdapter">FastAdapter</a> libraries we use in K-9 Mail</li> </ul> <p>I do most of my development work in <a href="https://www.jetbrains.com/idea/">IntelliJ IDEA</a>-based IDEs. The open source applications IntelliJ IDEA Community Edition and Android Studio provide all the functionality I need. But to do my part in ensuring the company behind IntelliJ IDEA and the creators of the <a href="https://kotlinlang.org/">Kotlin programming language</a> stick around, I purchased a license for IntelliJ IDEA Ultimate edition.</p> <h2 id="financials">Financials</h2> <p>Most of my income in 2020 came from the Prototype Fund project. But some of it came from people who donated money using one of these platforms:</p> <h3 id="github-sponsors">GitHub Sponsors</h3> <p>I signed up to the GitHub sponsors program as soon as I heard about it. On my <a href="https://github.com/sponsors/cketti">sponsors page</a> users could select one of the following tiers: $1 a month, $5 a month, $25 a month, $1000 a month. There are no rewards associated with any of the tiers.<br /> To no one’s surprise, nobody signed up for the $1000 tier. But people did sign up for all of the other tiers. In total I had 29 sponsors. Some of them canceled their sponsorship after a month or two, but most of them kept it going ❤️<br /> The total payout was <strong>966.82 EUR</strong>. Some of that was from GitHub matching contributions. But, honestly, I can’t tell if there’s a system when GitHub is matching contributions and when they’re not.</p> <h3 id="liberapay">Liberapay</h3> <p>In 2020 <a href="https://liberapay.com/k9mail">K-9 Mail’s Liberapay account</a> received <strong>4,794.91 EUR</strong> from ~250 patrons. That is more than double the amount received in 2019 📈</p> <h3 id="total">Total</h3> <p>That’s a total of <strong>5,761.73 EUR</strong> in donations. Not bad. But certainly not enough to live on in Germany. It looks like someone needs to come up with a better plan for 2021 😀</p> <h2 id="conclusion">Conclusion</h2> <p>I was disappointed with how my first year as a full-time open source developer went. I didn’t accomplish the one goal I had: releasing a new stable version of K-9 Mail.</p> <p>But writing this blog post made me feel better. It made me realize that small contributions add up to quite a bit. Another way to see that is by comparing my GitHub contribution graphs for 2019 and 2020.</p> <p><img src="/img/my-first-year-as-a-full-time-open-source-developer/github_contributions_2019.png" alt="GitHub contributions in 2019" /></p> <p><img src="/img/my-first-year-as-a-full-time-open-source-developer/github_contributions_2020.png" alt="GitHub contributions in 2020" /></p> <p>During the past 8 years I haven’t done much web development. Working on Jitsi Hacks made me realize that I still enjoy it quite a bit. I’ll probably end up doing more of that in the future.</p> <h2 id="plans-for-2021">Plans for 2021</h2> <p>I have quite a few ideas I want to work on this year. But my most important goals remain the same as they were in 2020:</p> <ul> <li>Finally release a new stable version of K-9 Mail.</li> <li>Figure out how to be a full-time open source developer without using up all of my savings.</li> </ul> <h2 id="thanks">Thanks</h2> <p>My thanks goes to everyone who has contributed to one of my projects, to the people who reviewed my blog posts, and to everyone who has helped me stay sane this last year.<br /> A special thanks goes to all of the sponsors who have made this journey possible ❤️</p> Thu, 14 Jan 2021 00:00:00 +0100 https://cketti.de/2021/01/14/my-first-year-as-a-full-time-open-source-developer/ https://cketti.de/2021/01/14/my-first-year-as-a-full-time-open-source-developer/ Avoid Intent.resolveActivity() <p>Until recently <a href="https://developer.android.com/reference/kotlin/android/content/pm/PackageManager"><code class="language-plaintext highlighter-rouge">PackageManager</code></a> allowed every app to access information about every other installed app. While this information is vital for apps like launchers, most apps have no need to know which other apps are installed. So Android 11 makes it a little bit harder to retrieve this information. For more details see <a href="https://developer.android.com/preview/privacy/package-visibility">Package visibility in Android 11</a>.</p> <p>Most apps should not be affected by this change because they don’t need to perform any <code class="language-plaintext highlighter-rouge">PackageManager</code> queries. But, sadly, many apps contain code to start <a href="https://developer.android.com/guide/components/intents-filters#Types">implicit intents</a> similar to this:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">intent</span> <span class="p">=</span> <span class="err">…</span> <span class="c1">// Some implicit intent, e.g. ACTION_VIEW</span> <span class="k">if</span> <span class="p">(</span><span class="n">intent</span><span class="p">.</span><span class="nf">resolveActivity</span><span class="p">(</span><span class="n">packageManager</span><span class="p">)</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span> <span class="nf">startActivity</span><span class="p">(</span><span class="n">intent</span><span class="p">)</span> <span class="c1">// or startActivityForResult(…)</span> <span class="p">}</span> </code></pre></div></div> <p>Due to the new limitations when targeting Android 11 the <a href="https://developer.android.com/reference/kotlin/android/content/Intent#resolveActivity(android.content.pm.PackageManager)"><code class="language-plaintext highlighter-rouge">Intent.resolveActivity()</code></a> call might now return <code class="language-plaintext highlighter-rouge">null</code> even though using <code class="language-plaintext highlighter-rouge">startActivity()</code> with the intent would work just fine.</p> <p>To make this work you could add a <code class="language-plaintext highlighter-rouge">&lt;queries&gt;</code> entry to <code class="language-plaintext highlighter-rouge">AndroidManifest.xml</code> as explained in the <a href="https://developer.android.com/preview/privacy/package-visibility">documentation</a>. But my suggestion is to avoid using <code class="language-plaintext highlighter-rouge">Intent.resolveActivity()</code> in such a case.</p> <h2 id="why-avoid-intentresolveactivity">Why avoid Intent.resolveActivity()?</h2> <p>I always cringe when people recommend this “<code class="language-plaintext highlighter-rouge">resolveActivity()</code> then <code class="language-plaintext highlighter-rouge">startActivity()</code>” pattern. You’re asking the system to perform the same operation twice. First “find the app to handle this Intent”, then “find the app to handle this Intent and then start that app using this Intent”.</p> <p>The problem with code like this is that the environment might change between the two method invocations. There might be a suitable app installed when <code class="language-plaintext highlighter-rouge">resolveActivity()</code> is executed and it could be gone by the time <code class="language-plaintext highlighter-rouge">startActivity()</code> is executed. Granted, in practice it is very unlikely to run into this case. Still, I think we can do better.</p> <h2 id="catch-activitynotfoundexception-instead">Catch ActivityNotFoundException instead</h2> <p>Instead of using <code class="language-plaintext highlighter-rouge">Intent.resolveActivity()</code> just call <code class="language-plaintext highlighter-rouge">startActivity()</code> and deal with <a href="https://developer.android.com/reference/kotlin/android/content/ActivityNotFoundException"><code class="language-plaintext highlighter-rouge">ActivityNotFoundException</code></a>.</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">intent</span> <span class="p">=</span> <span class="err">…</span> <span class="c1">// Some implicit intent</span> <span class="k">try</span> <span class="p">{</span> <span class="nf">startActivity</span><span class="p">(</span><span class="n">intent</span><span class="p">)</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">ActivityNotFoundException</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Display some error message</span> <span class="p">}</span> </code></pre></div></div> <p>In the past, using one method over the other was mostly a matter of taste. With the changes in Android 11 this method has the big advantage that you don’t have to add anything to <code class="language-plaintext highlighter-rouge">AndroidManifest.xml</code>. It just works.</p> <h2 id="always-avoid-intentresolveactivity">Always avoid Intent.resolveActivity()?</h2> <p>My advice only applies to this particular <code class="language-plaintext highlighter-rouge">startActivity()</code> use case. Of course <code class="language-plaintext highlighter-rouge">Intent.resolveActivity()</code> still has its uses. For example, you might want to hide a ‘take picture’ button if no camera app is installed. In that case <code class="language-plaintext highlighter-rouge">resolveActivity()</code> comes in handy.</p> Thu, 03 Sep 2020 00:00:00 +0200 https://cketti.de/2020/09/03/avoid-intent-resolveactivity/ https://cketti.de/2020/09/03/avoid-intent-resolveactivity/ Why I hide GitHub issue comments <p>I am the maintainer of a somewhat popular open source email app for Android, called <a href="https://k9mail.app/">K-9 Mail</a>. We use GitHub to host the app’s source code and use its <a href="https://github.com/k9mail/k-9/issues">issue tracker</a> to manage bug reports and feature requests.<br /> Lately, I’ve found myself hiding issue comments more and more. Sooner or later someone will ask “why?”. And now I can point them to this blog post 😀</p> <p>If you didn’t know that team members can <a href="https://docs.github.com/en/github/building-a-strong-community/managing-disruptive-comments#hiding-a-comment">hide comments</a>, this is what it looks like:</p> <p class="image-in-post"><img src="/img/why-i-hide-github-issue-comments/hidden_github_issue_comments.png" alt="Hidden GitHub issue comments" /></p> <p>There are all kinds of issue comments that I hide:</p> <ul> <li>“+1” comments</li> <li>Comments asking when this feature will (finally) be implemented</li> <li>Comments asking why this bug hasn’t been fixed yet</li> <li>“Please add this feature, it is really important to me” type of comments</li> <li>Totally off-topic comments where people ask general questions about the project</li> <li>“I have a similar but different problem” type of comments</li> <li>Comments where people (colorfully) express their dissatisfaction that a feature still hasn’t been implemented after X years</li> <li>My own comments telling people to please… <ul> <li>…use the <a href="https://forum.k9mail.app/">support forum</a> for tangential discussions</li> <li>…open a new issue because their bug/feature request is totally unrelated</li> </ul> </li> </ul> <p>All of these have one thing in common. For the developer who ends up implementing the feature or fixing the bug these comments don’t add any useful information.</p> <p>Especially issues for feature requests can remain open for years. When I go back to the issue page I don’t want to read through those unhelpful comments again (and again, and again). So that’s why I hide them.</p> <hr /> <p>I’d love to learn how other open source maintainers deal with such comments. Let me know on <a href="https://fosstodon.org/@cketti">Mastodon</a> or <a href="https://twitter.com/cketti">Twitter</a>.</p> <hr /> <p class="small">Thanks to <a href="https://twitter.com/tbsprs">@tbsprs</a> for proofreading this post.</p> Tue, 07 Jul 2020 00:00:00 +0200 https://cketti.de/2020/07/07/why-i-hide-github-issue-comments/ https://cketti.de/2020/07/07/why-i-hide-github-issue-comments/ android.net.MailTo is broken <p>The <a href="https://developer.android.com/reference/android/net/MailTo">MailTo</a> class has been part of the Android Platform API from the very start. And the class hasn’t been changed in a significant way since then. This could mean the API is very good and the code doesn’t contain any known bugs. Sadly, that’s not the case. <code class="language-plaintext highlighter-rouge">android.net.MailTo</code> is broken and shouldn’t be used. Let’s find out what’s wrong exactly.</p> <p>But I’m guessing most Android developers have never heard of this class before. So, first, let’s have a quick look at the documentation for <code class="language-plaintext highlighter-rouge">MailTo</code> to find out what it is about.</p> <blockquote> <p>This class parses a mailto scheme URL and then can be queried for the parsed parameters. This implements RFC 2368.</p> </blockquote> <p>And here’s a quick example of how the class is used:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">MailTo</span> <span class="n">mailToUri</span> <span class="o">=</span> <span class="nc">MailTo</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span> <span class="s">"mailto:[email protected]?subject=Hello%20World&amp;body=Hi"</span><span class="o">);</span> <span class="nc">String</span> <span class="n">recipient</span> <span class="o">=</span> <span class="n">mailToUri</span><span class="o">.</span><span class="na">getTo</span><span class="o">();</span> <span class="c1">// [email protected]</span> <span class="nc">String</span> <span class="n">subject</span> <span class="o">=</span> <span class="n">mailToUri</span><span class="o">.</span><span class="na">getSubject</span><span class="o">();</span> <span class="c1">// Hello World</span> <span class="nc">String</span> <span class="n">body</span> <span class="o">=</span> <span class="n">mailToUri</span><span class="o">.</span><span class="na">getBody</span><span class="o">();</span> <span class="c1">// Hi</span> </code></pre></div></div> <h2 id="what-is-broken-exactly">What is broken exactly?</h2> <p>The bugs are in <a href="https://android.googlesource.com/platform/frameworks/base/+/6090995951c6e2e4dcf38102f01793f8a94166e1/core/java/android/net/MailTo.java#64"><code class="language-plaintext highlighter-rouge">MailTo.parse()</code></a>. Let’s look at them one by one.</p> <h3 id="use-of-uriparse">Use of Uri.parse()</h3> <p>After making sure the supplied <code class="language-plaintext highlighter-rouge">url</code> starts with <code class="language-plaintext highlighter-rouge">mailto:</code>, <code class="language-plaintext highlighter-rouge">Uri.parse()</code> is invoked to parse the remainder of the <code class="language-plaintext highlighter-rouge">url</code> string.</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Strip the scheme as the Uri parser can't cope with it.</span> <span class="nc">String</span> <span class="n">noScheme</span> <span class="o">=</span> <span class="n">url</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="no">MAILTO_SCHEME</span><span class="o">.</span><span class="na">length</span><span class="o">());</span> <span class="nc">Uri</span> <span class="n">email</span> <span class="o">=</span> <span class="nc">Uri</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">noScheme</span><span class="o">);</span> <span class="c1">// …</span> <span class="nc">String</span> <span class="n">address</span> <span class="o">=</span> <span class="n">email</span><span class="o">.</span><span class="na">getPath</span><span class="o">();</span> </code></pre></div></div> <p>So if the input to <code class="language-plaintext highlighter-rouge">MailTo.parse()</code> was <code class="language-plaintext highlighter-rouge">mailto:[email protected]?subject=Hello</code>, <code class="language-plaintext highlighter-rouge">Uri.parse("[email protected]?subject=Hello")</code> is called.</p> <p>Now you might be asking yourself two things:</p> <ol> <li>Why is the <code class="language-plaintext highlighter-rouge">mailto:</code> prefix removed before passing the URL to <code class="language-plaintext highlighter-rouge">Uri.parse()</code>?</li> <li>Why doesn’t <code class="language-plaintext highlighter-rouge">Uri.parse()</code> complain that the input is missing a scheme identifier?</li> </ol> <p>The answer to the first question is that many <code class="language-plaintext highlighter-rouge">Uri</code> methods only work for hierarchical URIs. Those are URIs where the scheme identifier is followed by <code class="language-plaintext highlighter-rouge">://</code>, e.g. <code class="language-plaintext highlighter-rouge">https://authority/path?query#fragment</code>. If the <code class="language-plaintext highlighter-rouge">MailTo</code> class were using <code class="language-plaintext highlighter-rouge">Uri.parse("mailto:[email protected]?subject=Hello")</code> there would be no way to extract <code class="language-plaintext highlighter-rouge">[email protected]</code> using one of <code class="language-plaintext highlighter-rouge">Uri</code>’s methods.</p> <p>Reading the documentation (or source code) of the <code class="language-plaintext highlighter-rouge">Uri</code> class reveals the answer to the second question. The <code class="language-plaintext highlighter-rouge">Uri</code> class also supports relative URIs, i.e. ones without a scheme identifier. These are automatically treated as hierarchical URIs. And that’s why <code class="language-plaintext highlighter-rouge">email.getPath()</code> returns the recipient address <code class="language-plaintext highlighter-rouge">[email protected]</code>.</p> <p>Wanting the input to be parsed as relative URL is the source of the first bug. If one of the query parameters contains an unencoded <code class="language-plaintext highlighter-rouge">:</code> character, the input won’t be treated as a relative URL. Instead everything up to the colon will be treated as scheme identifier.</p> <p>Example:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Uri</span> <span class="n">parsed</span> <span class="o">=</span> <span class="nc">Uri</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="s">"[email protected]?body=oh:no"</span><span class="o">);</span> <span class="nc">String</span> <span class="n">scheme</span> <span class="o">=</span> <span class="n">parsed</span><span class="o">.</span><span class="na">getScheme</span><span class="o">();</span> <span class="c1">// Result: [email protected]?body=oh</span> <span class="nc">String</span> <span class="n">schemeSpecific</span> <span class="o">=</span> <span class="n">parsed</span><span class="o">.</span><span class="na">getSchemeSpecificPart</span><span class="o">();</span> <span class="c1">// Result: no</span> </code></pre></div></div> <p>To be fair, <a href="https://tools.ietf.org/html/rfc2368">RFC 2368</a> requires all URL reserved characters (and that includes <code class="language-plaintext highlighter-rouge">:</code>) in a header value to be percent-encoded (<code class="language-plaintext highlighter-rouge">%3A</code>). So an RFC 2368-compliant URL should look like this: <code class="language-plaintext highlighter-rouge">mailto:[email protected]?body=oh%3Ano</code>. However, this restriction has been relaxed in <a href="https://tools.ietf.org/html/rfc6068">RFC 6068</a> which replaced RFC 2368 in 2010. Now the colon and some other characters are allowed to be used without encoding inside header field values. This also frequently happens in real world scenarios. The result is that <code class="language-plaintext highlighter-rouge">mailto</code> URIs containing an unencoded <code class="language-plaintext highlighter-rouge">:</code> are not parsed properly by the <code class="language-plaintext highlighter-rouge">MailTo</code> class. And this is not limited to the header field containing the colon. None of the URI is parsed correctly.</p> <h3 id="use-of-urigetquery">Use of Uri.getQuery()</h3> <p>The next issue is the use of <code class="language-plaintext highlighter-rouge">Uri.getQuery()</code>.</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Parse out the query parameters</span> <span class="nc">String</span> <span class="n">query</span> <span class="o">=</span> <span class="n">email</span><span class="o">.</span><span class="na">getQuery</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">query</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">)</span> <span class="o">{</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">queries</span> <span class="o">=</span> <span class="n">query</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="s">"&amp;"</span><span class="o">);</span> <span class="k">for</span> <span class="o">(</span><span class="nc">String</span> <span class="n">q</span> <span class="o">:</span> <span class="n">queries</span><span class="o">)</span> <span class="o">{</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">nameval</span> <span class="o">=</span> <span class="n">q</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="s">"="</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">nameval</span><span class="o">.</span><span class="na">length</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span> <span class="k">continue</span><span class="o">;</span> <span class="o">}</span> <span class="c1">// insert the headers with the name in lowercase so that</span> <span class="c1">// we can easily find common headers</span> <span class="n">m</span><span class="o">.</span><span class="na">mHeaders</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">Uri</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="n">nameval</span><span class="o">[</span><span class="mi">0</span><span class="o">]).</span><span class="na">toLowerCase</span><span class="o">(</span><span class="nc">Locale</span><span class="o">.</span><span class="na">ROOT</span><span class="o">),</span> <span class="n">nameval</span><span class="o">.</span><span class="na">length</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="o">?</span> <span class="nc">Uri</span><span class="o">.</span><span class="na">decode</span><span class="o">(</span><span class="n">nameval</span><span class="o">[</span><span class="mi">1</span><span class="o">])</span> <span class="o">:</span> <span class="kc">null</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">Uri.getQuery()</code> returns the query part of the URI with percent-encoded characters decoded. So with <code class="language-plaintext highlighter-rouge">mailto:?body=Q%26A%3Dnice&amp;subject=Hi</code> as input we end up with <code class="language-plaintext highlighter-rouge">body=Q&amp;A=nice&amp;subject=Hi</code> in the variable <code class="language-plaintext highlighter-rouge">query</code>. Reading a bit further in the code snippet we can see how this becomes a problem when the string is split on the <code class="language-plaintext highlighter-rouge">&amp;</code> character. We end up with an array containing the elements <code class="language-plaintext highlighter-rouge">body=Q</code>, <code class="language-plaintext highlighter-rouge">A=nice</code>, <code class="language-plaintext highlighter-rouge">subject=Hi</code>, breaking the body text apart. The mistake is that <code class="language-plaintext highlighter-rouge">Uri.getQuery()</code> was used instead of <code class="language-plaintext highlighter-rouge">Uri.getEncodedQuery()</code> to get the encoded query.</p> <h3 id="double-decoding">Double decoding</h3> <p>After extracting the query parameters <code class="language-plaintext highlighter-rouge">Uri.decode()</code> is used to percent-decode the individual strings. But since <code class="language-plaintext highlighter-rouge">Uri.getQuery()</code> already decoded the query once, we now end up with double decoding. For example, the body value in <code class="language-plaintext highlighter-rouge">mailto:?body=%2525</code> is first decoded to <code class="language-plaintext highlighter-rouge">%25</code>, then again to <code class="language-plaintext highlighter-rouge">%</code>. This double decoding bug is only a side-effect of the previous bug. Once that one is fixed, the code that is decoding the query actually produces the expected result.</p> <h2 id="we-should-get-the-class-fixed-right">We should get the class fixed, right?</h2> <p>Over the years these bugs have been reported numerous times via Android’s issue tracker. They have either been closed during an issue tracker cleanup or closed because the issue wasn’t understood properly. I have created a new <a href="https://issuetracker.google.com/issues/153161703">issue</a> that includes a link to my <a href="https://github.com/cketti/MailToBugs">MailToBugs repository</a> which contains failing tests for all of these bugs.<br /> But even with a fix in a new Android version, apps would still need to work around the bugs as long as they want to support older platform versions. And we all know it would probably be 5+ years before we could drop the workarounds. 😞</p> <h3 id="fixed-version">Fixed version</h3> <p>If you are using the platform’s <code class="language-plaintext highlighter-rouge">MailTo</code> class and are looking for a drop-in replacement without these bugs, check out <del>my <a href="https://github.com/cketti/MailToCompat"><code class="language-plaintext highlighter-rouge">MailToCompat</code></a> library</del> <a href="https://developer.android.com/reference/kotlin/androidx/core/net/MailTo"><code class="language-plaintext highlighter-rouge">androidx.core.net.MailTo</code></a>.</p> <p><strong>Update:</strong> The fixed class will also be part of the <a href="https://developer.android.com/jetpack/androidx/releases/core">AndroidX Core</a> library starting with version 1.5.0-alpha02. Big thanks to <a href="https://twitter.com/ianhlake">Ian Lake</a> for suggesting I <a href="https://issuetracker.google.com/issues/159827506">make this a contribution to AndroidX Core</a> and then shepherding the change on <a href="https://android-review.googlesource.com/c/platform/frameworks/support/+/1349602">r.android.com</a> ❤️<br /> As soon as the class will be included in a stable release of AndroidX Core I’ll retire my <code class="language-plaintext highlighter-rouge">MailToCompat</code> library.</p> <p><strong>Update:</strong> Starting with AndroidX Core 1.5.0 the fixed <a href="https://developer.android.com/reference/kotlin/androidx/core/net/MailTo"><code class="language-plaintext highlighter-rouge">MailTo</code></a> class is available in stable releases of the library. So I marked my <code class="language-plaintext highlighter-rouge">MailToCompat</code> library as deprecated and archived the GitHub repository.</p> <h2 id="should-i-be-using-mailto">Should I be using <code class="language-plaintext highlighter-rouge">MailTo</code>?</h2> <p>Unless you’re writing an email client, probably not. Yet, I’ve seen lots of snippets that try to handle clicks on <code class="language-plaintext highlighter-rouge">mailto</code> URIs in a <code class="language-plaintext highlighter-rouge">WebView</code>. They look something like this:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// DON'T DO THIS</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">MyWebViewClient</span> <span class="kd">extends</span> <span class="nc">WebViewClient</span> <span class="o">{</span> <span class="c1">// …</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">shouldOverrideUrlLoading</span><span class="o">(</span><span class="nc">WebView</span> <span class="n">view</span><span class="o">,</span> <span class="nc">String</span> <span class="n">url</span><span class="o">)</span> <span class="o">{</span> <span class="nc">Uri</span> <span class="n">uri</span> <span class="o">=</span> <span class="nc">Uri</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">url</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">uri</span><span class="o">.</span><span class="na">getScheme</span><span class="o">().</span><span class="na">equalsIgnoreCase</span><span class="o">(</span><span class="s">"mailto"</span><span class="o">))</span> <span class="o">{</span> <span class="nc">MailTo</span> <span class="n">mailTo</span> <span class="o">=</span> <span class="nc">MailTo</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="n">url</span><span class="o">);</span> <span class="c1">// DON'T DO THIS</span> <span class="nc">Intent</span> <span class="n">intent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Intent</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">ACTION_SEND</span><span class="o">);</span> <span class="n">intent</span><span class="o">.</span><span class="na">setType</span><span class="o">(</span><span class="s">"message/rfc822"</span><span class="o">);</span> <span class="n">intent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">EXTRA_EMAIL</span><span class="o">,</span> <span class="k">new</span> <span class="nc">String</span><span class="o">[]</span> <span class="o">{</span> <span class="n">mailTo</span><span class="o">.</span><span class="na">getTo</span><span class="o">()</span> <span class="o">});</span> <span class="n">intent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">EXTRA_TEXT</span><span class="o">,</span> <span class="n">mailTo</span><span class="o">.</span><span class="na">getBody</span><span class="o">());</span> <span class="n">intent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">EXTRA_SUBJECT</span><span class="o">,</span> <span class="n">mailTo</span><span class="o">.</span><span class="na">getSubject</span><span class="o">());</span> <span class="n">intent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">EXTRA_CC</span><span class="o">,</span> <span class="n">mailTo</span><span class="o">.</span><span class="na">getCc</span><span class="o">());</span> <span class="n">context</span><span class="o">.</span><span class="na">startActivity</span><span class="o">(</span><span class="n">intent</span><span class="o">);</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span> <span class="c1">// …</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>That’s unnecessary. Email apps handle <code class="language-plaintext highlighter-rouge">mailto:</code> URIs just fine. <code class="language-plaintext highlighter-rouge">WebView</code>’s default behavior of using <code class="language-plaintext highlighter-rouge">ACTION_VIEW</code> with the URI works just fine. Alternatively, you could use <code class="language-plaintext highlighter-rouge">ACTION_SENDTO</code> with the unmodified URI. For more information check out my blog post <a href="https://cketti.de/2016/01/08/sending-email-using-intents/">Sending Email using Intents</a>.</p> <h2 id="summary">Summary</h2> <p>The fact that <code class="language-plaintext highlighter-rouge">android.net.MailTo</code> hasn’t been fixed in all those years is a good indicator that the class isn’t widely used. The main audience is authors of email clients. And we all had to either learn the hard way that we can’t rely on this class or never heard about it and went with our own implementation right from the start.</p> <p>The takeaway is that you shouldn’t be using <code class="language-plaintext highlighter-rouge">android.net.MailTo</code>. Instead use <code class="language-plaintext highlighter-rouge">androidx.core.net.MailTo</code>. And hopefully some day we can mark <code class="language-plaintext highlighter-rouge">android.net.MailTo</code> as <code class="language-plaintext highlighter-rouge">@Deprecated</code> and forget all about it. ⚰️</p> <hr /> <p><a href="https://github.com/cketti/cketti.github.io/commits/main/_posts/2020-06-22-android-net-mailto-is-broken.md">Update (2020-07-09)</a>: I updated the section “<a href="#fixed-version">Fixed version</a>” with information about the code being included in the AndroidX Core library.</p> <p><a href="https://github.com/cketti/cketti.github.io/commits/main/_posts/2020-06-22-android-net-mailto-is-broken.md">Update (2021-07-19)</a>: My <code class="language-plaintext highlighter-rouge">MailToCompat</code> library is now deprecated because <code class="language-plaintext highlighter-rouge">androidx.core.net.MailTo</code> is available in a stable release of the AndroidX Core library.</p> Mon, 22 Jun 2020 00:00:00 +0200 https://cketti.de/2020/06/22/android-net-mailto-is-broken/ https://cketti.de/2020/06/22/android-net-mailto-is-broken/ content:// URIs and OkHttp <p>Over a year ago I typed up an implementation of <a href="https://square.github.io/okhttp/">OkHttp</a>’s <a href="https://square.github.io/okhttp/4.x/okhttp/okhttp3/-request-body/"><code class="language-plaintext highlighter-rouge">RequestBody</code></a> that supports Android’s <code class="language-plaintext highlighter-rouge">content://</code> URIs. I called it <a href="https://gist.github.com/cketti/8ac927509787d7085a5ef8f866806f0f"><code class="language-plaintext highlighter-rouge">ContentUriRequestBody</code></a> and made it available as a <a href="https://gist.github.com/">Gist</a>. The other day <a href="https://gist.github.com/cketti/8ac927509787d7085a5ef8f866806f0f#gistcomment-3313081">a comment on the Gist</a> made me realize that this code might be helpful to more people than the person for whom it was originally created; probably more so when accompanied by some form of explanation. So here we go.</p> <h2 id="uploading-a-document">Uploading a document</h2> <p>Using Android’s Storage Access Framework to allow the user to select a document is fairly straight-forward. We do it like this:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">openDocumentIntent</span> <span class="p">=</span> <span class="nc">Intent</span><span class="p">(</span><span class="nc">Intent</span><span class="p">.</span><span class="nc">ACTION_OPEN_DOCUMENT</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span> <span class="n">type</span> <span class="p">=</span> <span class="s">"*/*"</span> <span class="nf">addCategory</span><span class="p">(</span><span class="nc">Intent</span><span class="p">.</span><span class="nc">CATEGORY_OPENABLE</span><span class="p">)</span> <span class="p">}</span> <span class="nf">startActivityForResult</span><span class="p">(</span><span class="n">openDocumentIntent</span><span class="p">,</span> <span class="nc">REQUEST_UPLOAD_DOCUMENT</span><span class="p">)</span> </code></pre></div></div> <p>When the user has selected a document we get the result in <code class="language-plaintext highlighter-rouge">onActivityResult()</code>. The data field of the result <code class="language-plaintext highlighter-rouge">Intent</code> contains a <code class="language-plaintext highlighter-rouge">content://</code> URI that can be used to access its contents.</p> <p>We’ll get back to this in a minute. First, let’s look at how to upload a file using an HTTP POST request with OkHttp.</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">request</span> <span class="p">=</span> <span class="nc">Request</span><span class="p">.</span><span class="nc">Builder</span><span class="p">()</span> <span class="p">.</span><span class="nf">url</span><span class="p">(</span><span class="s">"https://domain.example/upload"</span><span class="p">)</span> <span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="n">requestBody</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">response</span> <span class="p">=</span> <span class="n">okHttpClient</span><span class="p">.</span><span class="nf">newCall</span><span class="p">(</span><span class="n">request</span><span class="p">).</span><span class="nf">execute</span><span class="p">()</span> </code></pre></div></div> <p>So, we need a <code class="language-plaintext highlighter-rouge">RequestBody</code> instance that provides the content to be uploaded. If we look closer, we notice it is an abstract class and none of the implementations that come with OkHttp seem to fit our use case. But that can easily be fixed. There’s only two functions we need to implement:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">ContentUriRequestBody</span><span class="p">(</span> <span class="k">private</span> <span class="kd">val</span> <span class="py">contentResolver</span><span class="p">:</span> <span class="nc">ContentResolver</span><span class="p">,</span> <span class="k">private</span> <span class="kd">val</span> <span class="py">contentUri</span><span class="p">:</span> <span class="nc">Uri</span> <span class="p">)</span> <span class="p">:</span> <span class="nc">RequestBody</span><span class="p">()</span> <span class="p">{</span> <span class="k">override</span> <span class="k">fun</span> <span class="nf">contentType</span><span class="p">():</span> <span class="nc">MediaType</span><span class="p">?</span> <span class="p">{</span> <span class="kd">val</span> <span class="py">contentType</span> <span class="p">=</span> <span class="n">contentResolver</span><span class="p">.</span><span class="nf">getType</span><span class="p">(</span><span class="n">contentUri</span><span class="p">)</span> <span class="k">return</span> <span class="n">contentType</span><span class="o">?.</span><span class="nf">toMediaTypeOrNull</span><span class="p">()</span> <span class="p">}</span> <span class="k">override</span> <span class="k">fun</span> <span class="nf">writeTo</span><span class="p">(</span><span class="n">bufferedSink</span><span class="p">:</span> <span class="nc">BufferedSink</span><span class="p">)</span> <span class="p">{</span> <span class="kd">val</span> <span class="py">inputStream</span> <span class="p">=</span> <span class="n">contentResolver</span><span class="p">.</span><span class="nf">openInputStream</span><span class="p">(</span><span class="n">contentUri</span><span class="p">)</span> <span class="o">?:</span> <span class="k">throw</span> <span class="nc">IOException</span><span class="p">(</span><span class="s">"Couldn't open content URI for reading"</span><span class="p">)</span> <span class="n">inputStream</span><span class="p">.</span><span class="nf">source</span><span class="p">().</span><span class="nf">use</span> <span class="p">{</span> <span class="n">source</span> <span class="p">-&gt;</span> <span class="n">bufferedSink</span><span class="p">.</span><span class="nf">writeAll</span><span class="p">(</span><span class="n">source</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The <strong><code class="language-plaintext highlighter-rouge">contentType()</code></strong> function provides the media type (also called MIME type, or content type) for the content, e.g. <code class="language-plaintext highlighter-rouge">image/jpeg</code> or <code class="language-plaintext highlighter-rouge">text/plain</code>. Our <code class="language-plaintext highlighter-rouge">content://</code> URI can be queried for the media type of the document. This is done by calling <a href="https://developer.android.com/reference/android/content/ContentResolver#getType(android.net.Uri)"><code class="language-plaintext highlighter-rouge">ContentResolver.getType(contentUri)</code></a>.</p> <p>To send the actual bytes that make up the document the <strong><code class="language-plaintext highlighter-rouge">writeTo()</code></strong> function is called by OkHttp. Using <a href="https://developer.android.com/reference/android/content/ContentResolver#openInputStream(android.net.Uri)"><code class="language-plaintext highlighter-rouge">ContentResolver.openInputStream()</code></a> gives us a <code class="language-plaintext highlighter-rouge">java.io.InputStream</code> instance to read the data from. OkHttp is also using streams to read data from and write data to the network. But not the <code class="language-plaintext highlighter-rouge">java.io</code> API. Instead it is built on top of a library called <a href="https://square.github.io/okio/">Okio</a>, that implements the same concept, but in a much nicer way. Fortunately, Okio comes with an easy way to treat <code class="language-plaintext highlighter-rouge">java.io.InputStream</code> and <code class="language-plaintext highlighter-rouge">java.io.OutputStream</code> like the corresponding types in the Okio world, <a href="https://square.github.io/okio/2.x/okio/okio/-source/"><code class="language-plaintext highlighter-rouge">Source</code></a> and <a href="https://square.github.io/okio/2.x/okio/okio/-sink/"><code class="language-plaintext highlighter-rouge">Sink</code></a>. When calling the <code class="language-plaintext highlighter-rouge">writeTo()</code> function, OkHttp is handing us a <code class="language-plaintext highlighter-rouge">BufferedSink</code> and expects us to write the data to it. Since the document could be very large, we don’t want to read all of the bytes into memory and the write them to the <code class="language-plaintext highlighter-rouge">BufferedSink</code>. Instead we do what streams were designed to support: read a small amount of bytes from the document into memory, then write them to the <code class="language-plaintext highlighter-rouge">BufferedSink</code> so OkHttp can send them over the network. This step is repeated until we have read and subsequently written all of the document bytes. But because copying data from a source stream to a destination stream is such a common operation, there’s functionality in Okio that does all the heavy lifting for us. All we need to do is call the extension function <code class="language-plaintext highlighter-rouge">source()</code> to turn the <code class="language-plaintext highlighter-rouge">InputStream</code> we got from <code class="language-plaintext highlighter-rouge">ContentResolver.openInputStream()</code> into a <code class="language-plaintext highlighter-rouge">Source</code>. Then we call <a href="https://square.github.io/okio/2.x/okio/okio/-buffered-sink/write-all/"><code class="language-plaintext highlighter-rouge">BufferedSink.writeAll(Source)</code></a> and Okio will copy all bytes from the source to the sink.</p> <p>And that’s about it. This is more or less everything you need to send content referenced via a <code class="language-plaintext highlighter-rouge">content://</code> URI as body of an HTTP request using OkHttp.</p> <h2 id="writing-downloaded-content-to-a-document">Writing downloaded content to a document</h2> <p>Downloading content and writing it to a document is even easier because it doesn’t require us to extend a class. Displaying the system UI to allow the user to select a location and name for a new document is done like this:</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">openDocumentIntent</span> <span class="p">=</span> <span class="nc">Intent</span><span class="p">(</span><span class="nc">Intent</span><span class="p">.</span><span class="nc">ACTION_CREATE_DOCUMENT</span><span class="p">).</span><span class="nf">apply</span> <span class="p">{</span> <span class="n">type</span> <span class="p">=</span> <span class="s">"image/jpeg"</span> <span class="nf">addCategory</span><span class="p">(</span><span class="nc">Intent</span><span class="p">.</span><span class="nc">CATEGORY_OPENABLE</span><span class="p">)</span> <span class="nf">putExtra</span><span class="p">(</span><span class="nc">Intent</span><span class="p">.</span><span class="nc">EXTRA_TITLE</span><span class="p">,</span> <span class="s">"kitten.jpeg"</span><span class="p">)</span> <span class="p">}</span> <span class="nf">startActivityForResult</span><span class="p">(</span><span class="n">openDocumentIntent</span><span class="p">,</span> <span class="nc">REQUEST_DOWNLOAD_DOCUMENT</span><span class="p">)</span> </code></pre></div></div> <p>Again, we get a content URI in the result Intent delivered to <code class="language-plaintext highlighter-rouge">onActivityResult()</code>. We can then make an HTTP request and write the response to the document.</p> <div class="language-kotlin highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">val</span> <span class="py">request</span> <span class="p">=</span> <span class="nc">Request</span><span class="p">.</span><span class="nc">Builder</span><span class="p">()</span> <span class="p">.</span><span class="nf">url</span><span class="p">(</span><span class="s">"https://placekitten.com/300/300"</span><span class="p">)</span> <span class="p">.</span><span class="nf">build</span><span class="p">()</span> <span class="n">okHttpClient</span><span class="p">.</span><span class="nf">newCall</span><span class="p">(</span><span class="n">request</span><span class="p">).</span><span class="nf">execute</span><span class="p">().</span><span class="nf">use</span> <span class="p">{</span> <span class="n">response</span> <span class="p">-&gt;</span> <span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">isSuccessful</span><span class="p">)</span> <span class="p">{</span> <span class="n">response</span><span class="p">.</span><span class="n">body</span><span class="o">!!</span><span class="p">.</span><span class="nf">source</span><span class="p">().</span><span class="nf">use</span> <span class="p">{</span> <span class="n">bufferedSource</span> <span class="p">-&gt;</span> <span class="kd">val</span> <span class="py">outputStream</span> <span class="p">=</span> <span class="n">contentResolver</span><span class="p">.</span><span class="nf">openOutputStream</span><span class="p">(</span><span class="n">contentUri</span><span class="p">)</span> <span class="o">?:</span> <span class="k">throw</span> <span class="nc">IOException</span><span class="p">(</span><span class="s">"Couldn't open content URI"</span><span class="p">)</span> <span class="n">outputStream</span><span class="p">.</span><span class="nf">sink</span><span class="p">().</span><span class="nf">use</span> <span class="p">{</span> <span class="n">sink</span> <span class="p">-&gt;</span> <span class="n">bufferedSource</span><span class="p">.</span><span class="nf">readAll</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>It works similar to uploading, only this time we get a <code class="language-plaintext highlighter-rouge">java.io.OutputStream</code> instance when calling <a href="https://developer.android.com/reference/android/content/ContentResolver#openOutputStream(android.net.Uri)"><code class="language-plaintext highlighter-rouge">ContentResolver.openOutputStream()</code></a> with our <code class="language-plaintext highlighter-rouge">content://</code> URI. Again, there’s a handy extension function to turn this into one of Okio’s types, in this case <code class="language-plaintext highlighter-rouge">Sink</code>. The API surface of Okio’s <code class="language-plaintext highlighter-rouge">Source</code> and <code class="language-plaintext highlighter-rouge">Sink</code> types is rather limited. The really useful functionality comes with <code class="language-plaintext highlighter-rouge">BufferedSource</code> and <code class="language-plaintext highlighter-rouge">BufferedSink</code>. We could easily turn our <code class="language-plaintext highlighter-rouge">Sink</code> into a <code class="language-plaintext highlighter-rouge">BufferedSink</code> by using the extension function <code class="language-plaintext highlighter-rouge">buffer()</code>. Then we’d be able to call <code class="language-plaintext highlighter-rouge">BufferedSink.writeAll(Source)</code> like we did when uploading a document. But because OkHttp is already handing us a <code class="language-plaintext highlighter-rouge">BufferedSource</code>, we can also call <a href="https://square.github.io/okio/2.x/okio/okio/-buffered-source/read-all/"><code class="language-plaintext highlighter-rouge">BufferedSource.readAll(Sink)</code></a> to copy all bytes from the source to the sink.</p> <h2 id="sample-app">Sample app</h2> <p>I hope this post has helped you to understand how to make OkHttp play nice with <code class="language-plaintext highlighter-rouge">content://</code> URIs. Of course code snippets in blog posts always seem to be missing some crucial bits of information. So I created a small sample app that you can play around with: <a href="https://github.com/cketti/OkHttpWithContentUri">OkHttpWithContentUri</a></p> Sat, 23 May 2020 00:00:00 +0200 https://cketti.de/2020/05/23/content-uris-and-okhttp/ https://cketti.de/2020/05/23/content-uris-and-okhttp/ When URI permissions are in the way <p>Last year I read the blog post <a href="https://commonsware.com/blog/2016/09/07/notifications-sounds-android-7p0-aggravation.html">Notifications, Sounds, Android 7.0, and Aggravation</a> by the always well-informed Mark Murphy. In it Mark describes how the <a href="https://commonsware.com/blog/2016/03/14/psa-file-scheme-ban-n-developer-preview.html">ban on file: URIs</a> has made things difficult for developers who want to use custom notification sounds. Apparently, the notification subsystem wasn’t really prepared for <code class="language-plaintext highlighter-rouge">content:</code> URIs taking over. It’s currently not possible to use <a href="https://developer.android.com/reference/android/app/Notification.Builder.html#setSound(android.net.Uri)">Notification.Builder#setSound(Uri)</a> with a <code class="language-plaintext highlighter-rouge">content:</code> URI pointing to a <code class="language-plaintext highlighter-rouge">ContentProvider</code> that hasn’t been exported. That means <a href="https://developer.android.com/reference/android/support/v4/content/FileProvider.html"><code class="language-plaintext highlighter-rouge">FileProvider</code></a>, the standard solution when it comes to exposing files via <code class="language-plaintext highlighter-rouge">content:</code> URIs, won’t work.</p> <p>Mark lists a couple of options to work around the problem. Personally, I thought the option he named “The Axe”, using a custom <code class="language-plaintext highlighter-rouge">ContentProvider</code> without URI permissions, was the cleanest solution. For unrelated reasons I was reading the source code of <code class="language-plaintext highlighter-rouge">FileProvider</code> at that time. With the blog post in mind I thought it should be fairly easy to change the class to support that use case. And that’s what I did. The result was <a href="https://github.com/cketti/PublicFileProvider"><code class="language-plaintext highlighter-rouge">PublicFileProvider</code></a>, a modified version of <code class="language-plaintext highlighter-rouge">FileProvider</code> that doesn’t require URI permissions and that takes special care to only expose files read-only.</p> <p>For the last 7 months the project was gathering dust on GitHub. But the other day I stumbled across <a href="https://issuetracker.google.com/issues/36524161">this entry</a> in the bug tracker for the Android O Preview where Mark <a href="https://code.google.com/p/android/issues/detail?id=221899">reiterates</a> that the permission issue should be fixed in the platform rather than worked around by app developers. I agree, but I’m not confident this will be fixed in Android O. Even if it is, there’s still Android 7.0/7.1 we have to deal with. So there might still be some demand for something like <code class="language-plaintext highlighter-rouge">PublicFileProvider</code>. Which is why I added a bit more documentation and published the library on <a href="http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22public-fileprovider%22">Maven Central</a>. If you run into permission issues with custom notification sounds give <code class="language-plaintext highlighter-rouge">PublicFileProvider</code> a try.</p> <p>Check out <a href="https://github.com/cketti/PublicFileProvider">PublicFileProvider on GitHub</a>. Feedback is always welcome. Open an issue on GitHub or reach out via Twitter. I’m <a href="https://twitter.com/cketti">@cketti</a>. Please also let me know if you find another use case for this library 🙂</p> Mon, 03 Apr 2017 00:00:00 +0200 https://cketti.de/2017/04/03/when-uri-permissions-are-in-the-way/ https://cketti.de/2017/04/03/when-uri-permissions-are-in-the-way/ Share URL to Clipboard <p>On Android many apps allow the user to share the currently displayed content to another app. Often it’s a link to the content on the web together with some additional information, e.g. the headline and maybe the lead of a news article.</p> <h2 id="share-to-clipboard">Share to clipboard</h2> <p>A couple of weeks back I saw the following tweet in my timeline.</p> <blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr"> Hey native apps: if I share to clipboard, just give me the URL, not the URL &amp; &quot;hey kids check out this cool link&quot; or some other bullshit</p> &mdash; Jake Archibald (@jaffathecake) <a href="https://twitter.com/jaffathecake/status/726815002724847617">May 1, 2016</a> </blockquote> <script async="" src="//platform.twitter.com/widgets.js" charset="utf-8"></script> <p>Judging by the reactions, Jake is not the only one frustrated by this. So let’s have a closer look at how sharing to the clipboard works and how the requested feature can be implemented.</p> <h2 id="sharing-text-on-android">Sharing text on Android</h2> <p>Sharing content on Android is as simple as creating an Intent with the <a href="https://developer.android.com/reference/android/content/Intent.html#ACTION_SEND"><code class="language-plaintext highlighter-rouge">ACTION_SEND</code></a> action. You also have to specify the media type of the content. In this case we’re only interested in text, so the type is <code class="language-plaintext highlighter-rouge">text/plain</code> and the content itself goes into the <a href="https://developer.android.com/reference/android/content/Intent.html#EXTRA_TEXT"><code class="language-plaintext highlighter-rouge">EXTRA_TEXT</code></a> extra.</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Intent</span> <span class="n">shareIntent</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Intent</span><span class="o">();</span> <span class="n">shareIntent</span><span class="o">.</span><span class="na">setAction</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">ACTION_SEND</span><span class="o">);</span> <span class="n">shareIntent</span><span class="o">.</span><span class="na">setType</span><span class="o">(</span><span class="s">"text/plain"</span><span class="o">);</span> <span class="n">shareIntent</span><span class="o">.</span><span class="na">putExtra</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">EXTRA_TEXT</span><span class="o">,</span> <span class="s">"hey kids check out this cool link\n"</span> <span class="o">+</span> <span class="s">"https://example.org/cool-link"</span><span class="o">);</span> <span class="n">startActivity</span><span class="o">(</span><span class="nc">Intent</span><span class="o">.</span><span class="na">createChooser</span><span class="o">(</span><span class="n">shareIntent</span><span class="o">,</span> <span class="s">"Share with"</span><span class="o">));</span> </code></pre></div></div> <p>And that’s about all app developers have to do in order to show the Share dialog.</p> <h2 id="copy-to-clipboard">Copy to Clipboard</h2> <p>As you can see in the image below <em>“Copy to clipboard”</em> is an option in the list of available Share targets.</p> <p class="image-in-post"><img src="/img/share-url-to-clipboard/screenshot_share.png" alt="Share dialog" /></p> <p>What many people probably don’t know is that this is not a feature built into the Android platform. The <em>Copy to clipboard</em> action is <a href="http://www.androidpolice.com/2012/11/29/tip-drives-latest-update-adds-global-copy-to-clipboard-option-in-the-share-menu/">part of the Google Drive app</a> and was only added at the end of 2012.</p> <p>And of course there exist <a href="https://play.google.com/store/search?q=copy%20to%20clipboard">many</a> <a href="https://f-droid.org/repository/browse/?fdfilter=copy+to+clipboard">more</a> applications that provide the same functionality. So the easiest solution, sharing only the URL when the user selects <em>Copy to clipboard</em>, is not really feasible.</p> <h2 id="option-1-only-share-the-link">Option 1: Only share the link</h2> <p>The simplest way to satisfy the original request is to put only the URL into <code class="language-plaintext highlighter-rouge">EXTRA_TEXT</code> extra of the Share Intent.</p> <p>This way only the URL ends up in the clipboard when <em>Copy to Clipboard</em> is selected. But let’s say you want to send a link to a news article to a friend. In that case the headline and a short excerpt would surely be much appreciated by the recipient. And that’s the reason why most apps provide this additional text.</p> <h2 id="option-2-dont-share">Option 2: Don’t share</h2> <p>A possible way around this situation is to not use the Share facility and instead provide another way to copy a link to the clipboard. Twitter, for example, added a <em>“Copy link to Tweet”</em> entry to its menu.</p> <p class="image-in-post"><img src="/img/share-url-to-clipboard/screenshot_twitter_menu.png" alt="Twitter menu" /></p> <p>Functionality-wise this should make most users happy. Getting only the link is easy enough, while sharing uses additional text to provide some context.</p> <p>However, this can lead to a confusing user experience. By now users know that <em>Copy to Clipboard</em> can be found in the Share dialog and <a href="https://twitter.com/DasSurma/status/726815207918587908">don’t necessarily look elsewhere</a>.</p> <h2 id="option-3-extend-the-share-dialog">Option 3: Extend the Share dialog</h2> <p>Fortunately, it’s easy enough to add your own action to the Share dialog. So let’s add a <em>Copy link to clipboard</em> entry that does just that. While all regular Share targets receive the link and additional text.</p> <p>To make this work, we slightly modify the Share snippet from before.</p> <script src="https://gist.github.com/cketti/bf1f44cd21beb5061b772be339fd7fa7.js"></script> <p>Using <a href="https://developer.android.com/reference/android/content/Intent.html#EXTRA_INITIAL_INTENTS"><code class="language-plaintext highlighter-rouge">EXTRA_INITIAL_INTENTS</code></a> we can pass in a list of Intents that will be added to the Chooser dialog. In this case we point to an the Activity <code class="language-plaintext highlighter-rouge">CopyToClipboardActivity</code> that we still need to define.</p> <script src="https://gist.github.com/cketti/77a3ca308c7528f3fa6febeeedaa0fd0.js"></script> <p>In <code class="language-plaintext highlighter-rouge">onCreate()</code> we simply get the URL from the Intent that started the Activity and copy it to the clipboard. Then we finish the Activity right away to return to the screen where the Share event originated.</p> <p>The icon and label we give <code class="language-plaintext highlighter-rouge">CopyToClipboardActivity</code> in the app manifest are the ones being used by the Chooser dialog when displaying our custom entry. Using the <code class="language-plaintext highlighter-rouge">Theme.NoDisplay</code> theme disables the activity transition animation when our short-lived Activity is launched.</p> <script src="https://gist.github.com/cketti/1ea1b31fa3e75046ec0db2fdbc04f06e.js"></script> <p>And here is what it looks like to the user:</p> <p class="image-in-post"><img src="/img/share-url-to-clipboard/screenshot_share_with_copy_link.png" alt="Share dialog with 'Copy link to clipboard' option" /></p> <p>Of course there’s still the “Copy to clipboard” provided by the Drive app. And it stills copies all of the text to the clipboard. Maybe sometimes this is just what your users want. For all the other times, when it’s just the URL they’re after, the <em>Copy link to clipboard</em> option is right there at the top.</p> <p>The source code of a <a href="https://github.com/cketti/ShareUrlToClipboard">sample application</a> demonstrating all of this can be found on GitHub.</p> <h2 id="im-lazy-any-other-option">I’m lazy. Any other option?</h2> <p>As a developer, you, of course, created an app that is a shining example when it comes to copying links. But what about the other apps you use? Their creators are busy implementing all of your other feature requests. Now what?</p> <p>Fortunately, Android’s powerful Intent mechanism means you don’t actually need the cooperation of app authors to get the desired functionality. All you need is <a href="https://play.google.com/store/apps/details?id=nl.robwu.copylink">an app</a> that hooks into the Share dialog and then extracts links from the text shared to it. Writing such an app is easy enough and will be the topic of a future post.</p> <p>If you have other ideas on how to make copying links to the clipboard easier, I’d love to hear from you. I’m <a href="https://twitter.com/cketti">@cketti</a> on Twitter.</p> <p><a href="https://github.com/cketti/cketti.github.io/commits/master/_posts/2016-06-15-share-url-to-clipboard.md">Update</a>: Use <code class="language-plaintext highlighter-rouge">Theme.NoDisplay</code> instead of <code class="language-plaintext highlighter-rouge">Theme.Translucent.NoTitleBar</code> as <a href="https://twitter.com/devangelslondon/status/745403885041246209">suggested by @devangelslondon</a>.</p> Wed, 15 Jun 2016 00:00:00 +0200 https://cketti.de/2016/06/15/share-url-to-clipboard/ https://cketti.de/2016/06/15/share-url-to-clipboard/ Sending Email using Intents <p><em>This post was originally published on Medium: <a href="https://medium.com/@cketti/android-sending-email-using-intents-3da63662c58f#.m53umvu9z">Android: Sending Email using Intents</a></em></p> <p>Many things on Android are as easy as starting an Activity using the right Intent. Sending an email to a specific recipient is one of those things.</p> <p>Sadly, there is <a href="http://stackoverflow.com/questions/2197741/how-can-i-send-emails-from-my-android-application">much</a> <a href="http://www.mkyong.com/android/how-to-send-email-in-android/">bad</a> <a href="http://www.tutorialspoint.com/android/android_sending_email.htm">advice</a> out there on how to send emails using Intents. Because I write an open source <a href="https://github.com/k9mail/k-9">email app</a> I have a pretty good understanding of what goes on behind the scenes. That’s why I believe I know better than all those other people :)</p> <h2 id="action_sendto">ACTION_SENDTO</h2> <p>To start the Activity to compose an email in your user’s favorite email app you use an Intent with the <a href="https://developer.android.com/reference/android/content/Intent.html#ACTION_SENDTO"><code class="language-plaintext highlighter-rouge">ACTION_SENDTO</code></a> action. Unfortunately, the documentation is very sparse. It simply states:</p> <blockquote> <p><strong>Activity Action:</strong> Send a message to someone specified by the data.<br /> <strong>Input:</strong> getData() is URI describing the target.<br /> <strong>Output:</strong> nothing.</p> </blockquote> <p>This doesn’t mention email at all because <code class="language-plaintext highlighter-rouge">ACTION_SENDTO</code> is very generic and can also be used to send e.g. SMS, MMS or XMPP messages. To send an email we have to use a <em>mailto</em> URI as defined by <a href="https://tools.ietf.org/html/rfc6068">RFC 6068</a>. In its simplest form such a URI consists of <code class="language-plaintext highlighter-rouge">mailto:</code> followed by an email address, e.g. <code class="language-plaintext highlighter-rouge">mailto:[email protected]</code>. You might recognize this syntax from email links in HTML documents.</p> <p>Creating such an Intent might look like this:</p> <script src="https://gist.github.com/cketti/51938e906959e66d7d6d.js"></script> <p>Most of the time you want to provide more data than just a recipient. And the <em>mailto</em> specification allows us to also specify CC and BCC recipients, a subject, and even text for the email body.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mailto:[email protected][email protected]&amp;subject=Important%20message&amp;body=Hi%20there </code></pre></div></div> <p>Generating such a <em>mailto</em> URI is a bit tedious because you have to <a href="https://tools.ietf.org/html/rfc3986#section-2.1">percent-encode</a> certain characters as you can see in the example above. Android’s <a href="https://developer.android.com/reference/android/net/Uri.Builder.html"><code class="language-plaintext highlighter-rouge">Uri.Builder</code></a> class would be the perfect fit since it makes it easy to add query parameters while taking care of the encoding. Unfortunately, building Uri instances doesn’t work very well for non-hierarchical URIs like <em>mailto</em> URIs. One way to work around that is to manually construct the URI and dealing with the encoding.</p> <script src="https://gist.github.com/cketti/6a9b67dd92540bc74b2e.js"></script> <p><a href="http://stackoverflow.com/a/9462834/1800174">Some</a> <a href="http://stackoverflow.com/a/15022153/1800174">code</a> <a href="http://developer.android.com/guide/components/intents-common.html#Email">samples</a> advocate the use of <a href="https://developer.android.com/reference/android/content/Intent.html#EXTRA_SUBJECT"><code class="language-plaintext highlighter-rouge">EXTRA_SUBJECT</code></a>, <a href="https://developer.android.com/reference/android/content/Intent.html#EXTRA_TEXT"><code class="language-plaintext highlighter-rouge">EXTRA_TEXT</code></a>, etc. Those are certainly easier to use. However, the Android API reference only mentions them in connection with <a href="https://developer.android.com/reference/android/content/Intent.html#ACTION_SEND"><code class="language-plaintext highlighter-rouge">ACTION_SEND</code></a> and <a href="https://developer.android.com/reference/android/content/Intent.html#ACTION_SEND_MULTIPLE"><code class="language-plaintext highlighter-rouge">ACTION_SEND_MULTIPLE</code></a>. Those are more general Intents for sharing content to nobody in particular. The fact that the Extras also work with <code class="language-plaintext highlighter-rouge">ACTION_SENDTO</code> was an undocumented “feature” of AOSP Email and the Gmail app. That functionality then had to be copied by all other Android email clients because apps were and are using those Extras. But since all major email clients <a href="https://github.com/cketti/EmailIntentBuilder/wiki/EmailClientCompatibilityList">support</a> the use of query parameters in <em>mailto</em> URIs I see no reason to use an undocumented feature and advice against it.</p> <p>It is also worth mentioning that you shouldn’t use <a href="https://developer.android.com/reference/android/content/Intent.html#createChooser%28android.content.Intent,%20java.lang.CharSequence%29"><code class="language-plaintext highlighter-rouge">Intent.createChooser()</code></a> to launch an email Intent. Users most likely have a preferred email app that they want to be able to select as default app. The standard behavior when resolving an Intent deals with that just fine.</p> <p class="image-in-post"><img src="/img/sending-email-using-intents/screenshot_chooser.png" alt="Screenshot of chooser dialog" /><br /> Allow users to select a default app</p> <p>There are a couple of other things that need to be considered. Line breaks in the body, for example, need to be encoded as <code class="language-plaintext highlighter-rouge">%0D%0A</code> (CRLF). Because it’s no fun to deal with all of that when all you want to do is send an email, I wrote a small library to take care of the dirty details for you.</p> <h2 id="emailintentbuildermaking-lifeeasier">EmailIntentBuilder — Making life easier</h2> <p>The goal of the Email Intent Builder library is to make creating email Intents as easy as possible. You can find it on <a href="http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22de.cketti.mailto%22%20AND%20a%3A%22email-intent-builder%22">Maven Central</a>, the <a href="https://github.com/cketti/EmailIntentBuilder">source code</a> on GitHub.</p> <p>Building an email Intent is a simple matter of calling some methods on the builder:</p> <script src="https://gist.github.com/cketti/f4161c07ea9b7ae66f0d.js"></script> <p>Most of the time you also want to launch the Intent. The <code class="language-plaintext highlighter-rouge">start()</code> method will do that for you while also taking care of not throwing an exception if no app could be found to handle the Intent.</p> <script src="https://gist.github.com/cketti/988ff9de4b8460a9232d.js"></script> <h2 id="what-about-attachments">What about Attachments?</h2> <p>Unfortunately, the <code class="language-plaintext highlighter-rouge">ACTION_SENDTO</code> Intent doesn’t support attachments. If you need to send an email containing one or more attachments you should use Android’s more generic share mechanism, i.e. <code class="language-plaintext highlighter-rouge">ACTION_SEND</code> or <code class="language-plaintext highlighter-rouge">ACTION_SEND_MULTIPLE</code>. A lot of people have realized that this leads to quite a few more than just email apps showing up in the app chooser dialog. How to properly deal with that is material for another post.</p> <hr /> <p>I’d love to hear about your experiences dealing with email on Android. Let me know on <a href="https://twitter.com/cketti">Twitter</a>.</p> Fri, 08 Jan 2016 00:00:00 +0100 https://cketti.de/2016/01/08/sending-email-using-intents/ https://cketti.de/2016/01/08/sending-email-using-intents/