<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://manas.tungare.name/feed.xml" rel="self" type="application/atom+xml" /><link href="https://manas.tungare.name/" rel="alternate" type="text/html" /><updated>2026-08-08T08:45:55+00:00</updated><id>https://manas.tungare.name/feed.xml</id><title type="html">Manas Tungare</title><subtitle>Software Engineer @ Google • UberTL for Gmail Productivity • Ph.D. Computer Science • Love to code @chimbori.com.</subtitle><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><entry><title type="html">Continue to Write Well — Never Mind the AI Detectors</title><link href="https://manas.tungare.name/blog/continue-to-write-well-never-mind-the-ai-detectors" rel="alternate" type="text/html" title="Continue to Write Well — Never Mind the AI Detectors" /><published>2026-05-23T00:00:00+00:00</published><updated>2026-05-23T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/continue-to-write-well-never-mind-the-ai-detectors</id><content type="html" xml:base="https://manas.tungare.name/blog/continue-to-write-well-never-mind-the-ai-detectors"><![CDATA[<!--excerpt.start-->
<p>Yes, there’s an em-dash in the title. Yes, I wrote that. By hand. No, I will not stop using em-dashes in my regular writing.</p>

<p>I’m starting to see threads on Reddit and other places where people are alleging the use of AI simply because someone dared to write well, or used appropriate punctuation &amp; typography.
And then others reply, saying they’ve started introducing errors in their writing intentionally, to ward off AI Detector bots that seem to think that all well-written text must be AI-authored because humans don’t write that way.
<!--excerpt.end--></p>

<p>Reminds me of that old joke about a Japanese factory that received an order for widgets which said 3–4 out-of-spec widgets out of a 1000 would be OK, so they dutifully produced a batch with a note: “Our tooling was unable to manufacture the 3-4 broken pieces you asked for, so we had to craft them by hand; here they are.”</p>

<h3 id="the-ai-is-the-plagiarizer-not-you">The AI is the plagiarizer, not you</h3>

<p>AI writes like good writers, not the other way around.
LLMs were trained on our hand-written, painstakingly-edited text, so the AI is the plagiarizer, not you.
Don’t self-censor your own writing for fear of some half-assed AI Detector claiming you used AI to express your thoughts.</p>

<p>This is broken on so many levels. First, AI Detectors are made from the same probabilistic dust that AI Generators are made from.
(And it’s probably a VC-funded startup trying to <del>sell</del> hook you onto a monthly subscription to some snake oil.)
There’s really no way to tell with any amount of certainty whether some text was machine-generated or human-authored. (Insert spiderman meme here, with multiple spidermen pointing fingers at one another).</p>

<p>Second, bowing to these stochastic parrots only serves to reinforce the wrong stereotype that only an LLM can produce good prose.
This is a false meme we need to fight back against, and the sooner the better.</p>

<h3 id="good-writing-matters-good-typography-matters">Good Writing Matters; Good Typography Matters</h3>

<p>I’ve been using proper typography in my writing for decades, and I’m not about to stop any time soon.
Ever since TeX introduced me to em-dashes (<code class="language-plaintext highlighter-rouge">—</code>), en-dashes (<code class="language-plaintext highlighter-rouge">–</code>), proper left- (<code class="language-plaintext highlighter-rouge">‘</code>) and right-quotes (<code class="language-plaintext highlighter-rouge">’</code>), and my personal favorite, the interrobang (<code class="language-plaintext highlighter-rouge">‽</code>), I’ve been making sure to use the correct typographical entities in all my writing.</p>

<p>Not just in academic papers or formal blog posts; if I’ve sent you an email or chat in the last ~20 years, you can go back and confirm that I used right-quotes for apostrophes (like this: <code class="language-plaintext highlighter-rouge">don’t</code>, not <code class="language-plaintext highlighter-rouge">don't</code>), and proper left-double-quotes and right-double-quotes for quotations (like <code class="language-plaintext highlighter-rouge">“hello”</code>, not <code class="language-plaintext highlighter-rouge">"hello"</code>), not straight double-quotes. (Those are reserved for programming use cases.)</p>

<p>These are only a tiny bit harder to type than the simpler characters that show up on a single key on your keyboard.
E.g. on macOS,</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">“</code> is <code class="language-plaintext highlighter-rouge">Option + [</code></li>
  <li><code class="language-plaintext highlighter-rouge">”</code> is <code class="language-plaintext highlighter-rouge">Option + Shift + [</code></li>
  <li><code class="language-plaintext highlighter-rouge">–</code> is <code class="language-plaintext highlighter-rouge">Option + -</code></li>
  <li><code class="language-plaintext highlighter-rouge">—</code> is <code class="language-plaintext highlighter-rouge">Option + Shift + -</code></li>
</ul>

<p>It’s even easier on Android and iOS: just hold down the corresponding keyboard keys for longer than usual, and it’ll show you a pop-up with a palette of symbols.</p>

<p><a href="https://en.wikipedia.org/wiki/TeX" target="_blank">TeX</a> deserves credit for introducing me to proper typography, and for showing why it matters so much.
TeX makes it automatic to the point where using proper typography is the well-lit path, and using bad/wrong typography takes additional effort.
When you write three hyphens in LaTeX (—), it automatically produces an em-dash.
Two hyphens (–) become an en-dash.</p>

<div class="highlighted-box">
  <p>Aside: I admire
<a href="https://en.wikipedia.org/wiki/Donald_Knuth" target="_blank">Donald Knuth</a>,
the creator of TeX, as the ultimate yak shaver in Computer Science:
he ended up creating TeX in ~1978 because he didn’t like the typography tools available at the time he wrote
“<a href="https://en.wikipedia.org/wiki/The_Art_of_Computer_Programming" target="_blank">The Art of Computer Programming</a>”.
So not only did we get a classic tome out of him, we also got a typesetting system that has survived the test of time for nearly 5 decades now.</p>
</div>

<p><a href="https://www.reddit.com/r/keming/top/?screen_view_count=2&amp;t=all" target="_blank">Good typography matters</a>.
Even though your readers might not notice your attention to detail at every word, they can tell when something looks polished, and when something feels off.
Typefaces matter, design matters.
And writing well continues to matter.</p>

<h3 id="the-actual-advice">The Actual Advice</h3>

<p>The reason we’ve come to associate em-dashes and en-dashes with LLM-generated text is because LLMs tend to over-use them.
An LLM will use an em-dash even where a comma would be more appropriate.
It’s like a child who just learned a new word and wants to use it every chance they get.</p>

<p>Be the human writer who understands the nuances among the different kinds of hyphens/dashes.</p>

<p>Maintain your own voice &amp; style, and feel free to use AI to correct typos and punctuation. But don’t let it think for you, or switch up the punctuation to use uncommon marks where they don’t belong.</p>

<p>Most importantly, don’t dumb down your own writing—both prose and typography—to avoid being flagged as an LLM.</p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[Don’t break your own natural writing style just because of the fear of an AI Detector bot alleging plagiarism.]]></summary></entry><entry><title type="html">Vibe Coding for Engineers: The Good Parts (continued)</title><link href="https://manas.tungare.name/blog/vibe-coding-the-good-parts-continued" rel="alternate" type="text/html" title="Vibe Coding for Engineers: The Good Parts (continued)" /><published>2026-01-02T00:00:00+00:00</published><updated>2026-01-02T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/vibe-coding-the-good-parts-continued</id><content type="html" xml:base="https://manas.tungare.name/blog/vibe-coding-the-good-parts-continued"><![CDATA[<p><em>If you haven’t read the <a href="vibe-coding-the-good-parts" class="btn-blue">first part</a>, check that out first.</em></p>

<blockquote>
  <p>From an AI skeptic turned cautiously-optimistic vibe-coding engineer, here are a few specific engineering tasks that can be accelerated using AI.</p>
</blockquote>

<h2 id="slightly-larger-tasks">Slightly Larger Tasks</h2>

<!--excerpt.start-->
<p>After the brief warm-up with <a href="vibe-coding-the-good-parts-2">smaller tasks</a>, I started delegating slightly larger tasks to LLMs.
<!--excerpt.end--></p>

<h3 id="stuff-i-dont-use-every-day">Stuff I don’t use every day</h3>

<p>For a couple projects in Go, I needed to author more complex SQL queries than I’m used to.
I haven’t needed to do SQL in any advanced capacity for my day job for a while, so my syntactical knowledge has become a bit rusty.
Having an LLM provide a fast first draft ended up being a good accelerator.
Of course I made my own edits to it, but the overall structure was better than I might have created myself.</p>

<h3 id="llms-as-a-better-integrated-stackoverflow">LLMs as a better-integrated StackOverflow</h3>

<p>A lot of my browser history has been filled with Stack Overflow links. Some of them useful, many not.
LLMs are a game-changer here, because in addition to having ingested all of StackOverflow, they’re also aware of the very specific context of the code that you are trying to write.
And it saves me the effort of looking at my error messages, verbalizing them into a question, pasting it into Google, clicking on to StackOverflow, browsing through a bunch of 10-year-old code snippets, and then coming back and tweaking my source until it works.</p>

<p>The “edit → compile → test → lookup error message → go to StackOverflow → fix code” loop is much faster with LLMs.</p>

<h3 id="writing-tests">Writing tests</h3>

<p>For every person who loves to write tests, there are 10 others who don’t.
Vibe coding is a great aid for those other 10.
Are all the generated tests of decent quality? Mostly yes, but some may not be.
But the fact that tests exist is by itself a big deal.</p>

<p>For personal projects, I’ve often skipped writing proper tests, but with LLMs I have no excuse not to.
I do look over all the generated tests and typically find a few things to tweak or a few more things to add, but having the scaffolding and the skeleton already set up, built, and tested makes that job a lot easier.</p>

<h3 id="handling-edge-cases-from-the-get-go">Handling edge cases from the get-go</h3>

<p>Good test cases already include proactively testing edge cases.
But even when asked to author code, I found that agents were better at proactively recognizing and handling edge cases.</p>

<p>If I were the one writing that code, I’d likely have done a first pass for basic functionality, then a second pass to cover all the exceptional cases.
The LLM handled it in a single pass, even without having asked it explicitly to do so.</p>

<h3 id="unexpected-win-migrating-code-from-one-language-to-another">Unexpected win: Migrating code from one language to another</h3>

<p>I provided the LLM with a skeleton for a Go language project (that I had developed manually) and an older project of mine using the <a href="https://ktor.io/">Ktor</a> framework for Kotlin, and asked it to migrate the Ktor code to its Go equivalent.</p>

<p>It was surprisingly accurate and actually worked well.
It did not compile at the first attempt, but the things that were causing it to not compile were absolutely trivial to fix.
From beginning to end, it took me about one tenth of the time to use the LLM to bootstrap the project than it would have otherwise.
In fact, if it weren’t for the LLM, I would have procrastinated on this migration for far longer, and likely never actually have gotten around to it in the first place. So that’s a win right there.</p>

<p><span class="swash"></span></p>

<p>Hopefully that’s enough encouragement for you to try this out.</p>

<p>It’s not all rosy, though, and here’s what I found didn’t work well for me.</p>

<h2 id="a-quick-discussion-of-the-bad-parts">A quick discussion of the Bad Parts</h2>

<h3 id="dont-trust-it-blindly">Don’t trust it blindly</h3>

<p>Doing a deep, thorough code review of whatever it generates is essential to getting the most out of this tool.
Think of it as an IDE on steroids.
Or a junior engineer who is still learning the ropes.
You would not let them commit code to production without any oversight, so don’t do that with an LLM either.
Obvious issues are security-related and the lack of maintainability when LLMs keep piling on bad code on top of bad code.</p>

<h3 id="auto-completions-are-super-annoying">Auto completions are super annoying</h3>

<p>And mostly wrong. I have turned off auto-completions altogether, only access LLMs in explicit Agent Mode (through the side panel, mostly).</p>

<p>Low-quality LLM-powered auto-completions were worse than having none because they were actively distracting me from whatever I was trying to think and type, and causing confusion in my mind, slowing me down.</p>

<h3 id="source-control-is-your-friend">Source control is your friend</h3>

<p>Although you have the option to accept or reject changes after each agent interaction, it goes without saying that you need proper source control to ensure that the changes that the LLM agent is making are sane.
Only after a thorough code review does a commit get made.
Any changes made after the last commit are easily reviewable in isolation.
This is not unique to LLMs or vibe coding; this is just how good software engineering should already be.</p>

<h2 id="next-steps">Next Steps</h2>

<p>I haven’t yet fully given the LLM full reign of my projects.
A lot of people have an LLM generate an architecture and an execution plan and have it go run with it without supervision;
I haven’t quite gotten that far yet; that’s a topic to explore further in 2026!
So far I have only delegated mid-level to lower-level tasks to the LLM and I’m sure at some point I will feel comfortable enough to go a couple levels higher as well.</p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[From an AI skeptic turned cautiously-optimistic vibe-coding engineer, here’s how you can get started with LLM-assisted engineering.]]></summary></entry><entry><title type="html">Vibe Coding for Engineers: The Good Parts</title><link href="https://manas.tungare.name/blog/vibe-coding-the-good-parts" rel="alternate" type="text/html" title="Vibe Coding for Engineers: The Good Parts" /><published>2026-01-01T00:00:00+00:00</published><updated>2026-01-01T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/vibe-coding-the-good-parts</id><content type="html" xml:base="https://manas.tungare.name/blog/vibe-coding-the-good-parts"><![CDATA[<!--excerpt.start-->
<p>Pretty much every technology has had its <a href="https://www.oreilly.com/library/view/javascript-the-good/9780596517748/">good parts</a> and its bad parts.
Vibe coding may be new to the game, but it shares more than a few fundamental characteristics with others that came before it.
From an AI skeptic turned cautiously-optimistic vibe-coding engineer, here are a few specific engineering tasks that can be accelerated using AI.
<!--excerpt.end--></p>

<p>LLM-assisted programming (as with all other things AI) is clearly polarizing, but whether you like it or hate it, you won’t be able to ignore it for long.
After having been a skeptic of automatically-generated code for a while, I begrudgingly gave it a try sometime around an year ago.
There’s a lot of how-to discussion online about vibe coding for non-engineers, but not much for engineers. (“vibe engineering”?)</p>

<h3 id="but-hear-me-out">But… hear me out</h3>

<p>You’ll find a lot of threads on social media about managers forcing engineers to use LLMs, which has the opposite of the intended effect, because nobody likes being dictated to, especially about their own areas of expertise, and especially from people with less perceived expertise in those exact areas.</p>

<div class="highlighted-box">
  <p>The output of an LLM may not be perfect, but think of it as taking public transit to a faraway destination instead of walking all the way.
You get on the bus, you end up in the general vicinity of where you wanted to go, then you get off the bus and walk the last few steps yourself.</p>

  <p>If you don’t walk that last bit, you won’t end up exactly where you wanted to go.
But if you insist on walking all the way, you’d take a lot longer to get there.</p>

  <p>Conversely, if you keep taking one wrong bus after another, you’ll get completely lost.
Don’t just vibe-code without checking everything the LLM generates.</p>
</div>

<p>At the end of the day, treat it as the tool it is.
Use it for what it does well.
And skip using it where it doesn’t add value.</p>

<p>This article is an attempt to catalog my experiences of what I found it to be actually good at. It’s also a path to slowly getting used to LLM-driven engineering, one small step at a time.</p>

<p>This is authored from the perspective of an engineer who knows how to code and is mostly using an LLM to speed things up.
If that describes you, great!
If you are not familiar with coding or are just beginning to learn, I strongly encourage you to spend the time to understand the language and framework of your choice. Because in the long run, that will get you much farther than an LLM ever can.</p>

<h3 id="my-setup">My setup</h3>

<p>Just so you can contextualize this, here’s my setup. The precise set of tools I used is not critical;
the broader discussion applies well no matter what tools you’re using.</p>

<ul>
  <li>
    <p>For personal projects,
I’ve been using GitHub Copilot with Gemini 3 Pro as well as Claude Sonnet/Opus 4.5 in VS Code and in JetBrains IDEs (mostly Android Studio, but also IntelliJ IDEA).
Since most of my projects are solo side projects, there’s no code review involved.</p>
  </li>
  <li>
    <p>At work, I’ve used various internal tools that all rely on models from the Gemini family and integrate with Google’s internal code authoring &amp; code review tools.</p>
  </li>
</ul>

<p><span class="swash"></span></p>

<h2 id="start-small">Start small</h2>

<p>If you’re just getting started, it might feel overwhelming to trust your full workflow to an LLM.
The best thing is to start small with low-risk low-impact assistance before you move on to more hands-off Agent Mode tasks.</p>

<h3 id="generate-commit-messages">Generate commit messages</h3>

<p>I’ve found automating commit messages to be an excellent way to get your feet wet.
You’re still writing all the code, but by letting the LLM draft up commit messages, you can see for yourself how well it understands what you’re trying to do (or not), which may (or may not) eventually convince you to let go and let it drive deeper, more involved tasks independently.</p>

<p><strong>Unexpected change in my behavior:</strong> After delegating commit messages to an LLM, I found myself creating smaller commits:
E.g. previously, I often updated multiple dependencies at one go: grouped into a single (or a few) commits.
Now I typically create one commit per dependency, and the LLM-generated commit message includes details of exactly what changed, and the version each dependency was updated to.
Doing this manually would not have been the best use of my time, but with LLMs, the effort I spend is lower than some reasonable threshold, so this has become worthwhile.</p>

<p>(Why not <a href="https://github.blog/news-insights/product-news/keep-all-your-packages-up-to-date-with-dependabot/">dependabot</a>? Because I like to test each change before committing it, even for minor semver updates).</p>

<h3 id="automate-boilerplate-code">Automate boilerplate code</h3>

<p>One clear win I found for vibe coding was to generate boilerplate: config files, scaffolding, that kind of thing.
An LLM by definition is good at pattern recognition and pattern repetition, so boilerplate is something it can accomplish pretty quickly.</p>

<p>(Of course if you can eliminate boilerplate entirely, that should be the first thing you do.)</p>

<h3 id="generate-extremely-simple-code-snippets">Generate extremely simple code snippets</h3>

<p>When I know the exact logic I want to use for a particular function, vibe coding is like a <a href="https://google.com/search?q=bicycle+for+the+mind">bicycle for the mind</a>.
The kind of thing you’d say to an intern, and they come back a few hours later with code that does what you wanted.
Maybe not in the exact same way you’d have done it, but that’s where you come in.</p>

<h3 id="automated-migration-from-deprecated-code">Automated migration from deprecated code</h3>

<p>I begrudgingly deal with Gradle for Android development, but that tool is generally a nightmare to deal with, with constantly changing APIs. Every time I update to the latest Gradle version, a bunch of things are deprecated, and a bunch of things stop working. Now I just paste the error message into an LLM, and have it fix it. It generally works.</p>

<h3 id="extract-meaningful-info-from-screenshots-via-ocr">Extract meaningful info from Screenshots via OCR</h3>

<p>I have also found a good use case for personal (non-programming) tasks.</p>

<p>While <a href="https://github.com/tesseract-ocr/tesseract">Tesseract</a> &amp; others are high-quality tools of the pre-LLM era, LLMs have more than caught up, and are able to extract info in a better structured way than plain OCR-based tools ever did.</p>

<p>You can paste a screenshot into an LLM as context.
Ask it to extract information from it.
Then act on it.
And paste the processed results into your source code or a data file.
All of this is much quicker than massaging that same data by hand over multiple steps.</p>

<p>Taking screenshots of (HTML) tables on the Web and pasting them into a spreadsheet is made much easier by passing it through an LLM.
You would think that pre-LLM tools would also be good at this kind of structured yet mechanical copy/paste &amp; format conversion, but LLMs are miles ahead.</p>

<p>Continue on to the <a href="vibe-coding-the-good-parts-continued" class="btn-blue">next part</a></p>

<p><em>(This got too long for a single post, so I split it up into two.)</em></p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[From an AI skeptic turned cautiously-optimistic vibe-coding engineer, here’s how you can get started with LLM-assisted engineering.]]></summary></entry><entry><title type="html">How to NOT get Promoted</title><link href="https://manas.tungare.name/blog/how-to-not-get-promoted" rel="alternate" type="text/html" title="How to NOT get Promoted" /><published>2025-11-14T00:00:00+00:00</published><updated>2025-11-14T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/how-to-not-get-promoted</id><content type="html" xml:base="https://manas.tungare.name/blog/how-to-not-get-promoted"><![CDATA[<!--excerpt.start-->
<p>There are a lot of guides &amp; docs about what you should do to <strong>get</strong> promoted,
but not much about the typical mistakes most people make, despite following all that other advice.
<!--excerpt.end--></p>

<div class="highlighted-box">
<p><strong>Why this doc?</strong>
  I originally posted this doc internally at Google, where I’ve served as Manager and IC for 16+ years
  &amp; participated in dozens of promo committees.
  I’ve seen many solid promo attempts fail because the right evidence was not presented in a convincing way.
  This post is a terse distillation of repeated patterns I’ve observed, and was requested by many to post it publicly.
</p>
<p>
  <em>This doc is from the perspective of Software Engineers; including Individual Contributors (ICs), Tech Leads (TLs), and Managers (TLMs).
  The vocabulary &amp; expectations may not apply to other fields.</em>
</p>
</div>

<h2 id="in-order-to-not-get-promoted-">In order to not get promoted, …</h2>

<h3 id="how-you-work">How You Work</h3>

<ul>
  <li>Read all the design docs
    <ul>
      <li>While not making forward progress on the specific task assigned to you.</li>
    </ul>
  </li>
  <li>Build an awesome system
    <ul>
      <li>Without writing a design doc for it first.</li>
    </ul>
  </li>
  <li>Start a design doc
    <ul>
      <li>But never finish it.</li>
    </ul>
  </li>
  <li>Finish a design doc
    <ul>
      <li>But build something completely different, and never update the design doc afterwards.</li>
    </ul>
  </li>
  <li>Write a design doc
    <ul>
      <li>But skip sharing it with others on your team.</li>
    </ul>
  </li>
  <li>Write a design doc and share it with others
    <ul>
      <li>But fail to respond to any of the comments left by others.</li>
    </ul>
  </li>
  <li>Write a design doc, respond to comments
    <ul>
      <li>But skip following up with approvers/reviewers, leaving the doc unapproved for a long time.</li>
    </ul>
  </li>
  <li>Write a design doc, follow up with approvers/reviewers and discuss open comments offline
    <ul>
      <li>But skip documenting the final resolution with rationale and closing the comments</li>
    </ul>
  </li>
  <li>Write a design doc
    <ul>
      <li>The week before Promo Packets are due (yes, I have seen this happen and it’s obvious from version history…)</li>
    </ul>
  </li>
  <li>Write great design docs
    <ul>
      <li>But fail to set permissions so the promo committee could view the doc + version history + resolved comments.</li>
    </ul>
  </li>
</ul>

<h3 id="how-you-collaborate">How You Collaborate</h3>

<ul>
  <li>Be a great engineer
    <ul>
      <li>But an awful collaborator. (At higher levels, your collaboration skills become more important than pure technical skills.)</li>
    </ul>
  </li>
  <li>Be a thorough code reviewer
    <ul>
      <li>But nitpick to the point that folks can’t submit any code</li>
    </ul>
  </li>
  <li>Do community work: interviews, mentorship, hiring committee, etc.
    <ul>
      <li>But not have enough time left over for core technical contributions</li>
    </ul>
  </li>
  <li>Do a lot of <a href="https://www.noidea.dog/glue">glue work</a>
    <ul>
      <li>But only glue work, ignoring core technical contributions on which you’ll be evaluated</li>
    </ul>
  </li>
  <li>Do your own tasks diligently
    <ul>
      <li>But refuse to help others because your plate is always full</li>
    </ul>
  </li>
  <li>Contribute to a bunch of random projects
    <ul>
      <li>But fail to be recognized as an owner / authority / point of contact for any one of them</li>
    </ul>
  </li>
  <li>Attend a lot of meetings
    <ul>
      <li>With little to show for the time spent in them</li>
    </ul>
  </li>
  <li>Take full credit for partial contributions
    <ul>
      <li>Promo committees can easily see if two or more people are taking credit for largely-overlapping work.
Be precise in what you are claiming as your own, &amp; explain the role of collaborators if you want to get promoted.</li>
    </ul>
  </li>
</ul>

<h3 id="launches">Launches</h3>

<ul>
  <li>Run into an issue during launch
    <ul>
      <li>But instead of highlighting how you fixed it, what you learnt and how you grew from it, attempt to cover it up in the promo packet.</li>
    </ul>
  </li>
  <li>Stay up nights and weekends
    <ul>
      <li>Without identifying or articulating process changes to make it unnecessary to stay up nights and weekends in the future.</li>
    </ul>
  </li>
  <li>Build and launch an awesome system
    <ul>
      <li>But apply for promo before the launch has been validated in Production (no metrics ⇒ no promo)</li>
    </ul>
  </li>
  <li>Build and launch an awesome system with metrics
    <ul>
      <li>But fail to explain why it was impactful, or why the metrics matter</li>
    </ul>
  </li>
  <li>Apply for promotion with a decently-strong packet
    <ul>
      <li>But 6 months too early, while there are still gaps in your next-level readiness in specific areas</li>
    </ul>
  </li>
</ul>

<h3 id="manager-related">Manager Related</h3>

<ul>
  <li>Have a career goal or career development questions in mind
    <ul>
      <li>But fail to have regular in-depth career conversations with your manager</li>
    </ul>
  </li>
  <li>Work on projects assigned by your manager
    <ul>
      <li>Even though you hate them, without providing this feedback to your manager so they could instead reassign you to something you are passionate about</li>
    </ul>
  </li>
  <li>Have a perfect story crafted for your career trajectory
    <ul>
      <li>But end up reporting to a manager who cannot convincingly narrate it to a committee</li>
    </ul>
  </li>
  <li>Have a great packet written by your manager
    <ul>
      <li>But fail to get feedback from a few others to make sure it’s airtight</li>
    </ul>
  </li>
  <li>Be the senior-most person in every room
    <ul>
      <li>With no senior collaborators who can attest to your readiness at the next level</li>
    </ul>
  </li>
</ul>

<h3 id="mindset">Mindset</h3>

<ul>
  <li>Obsess about promotion
    <ul>
      <li>Such that every decision at work becomes about whether it will get you promoted or not (Seriously, this does not help at all. Makes things worse for you, your manager, and your team if promo is the only thing on your mind).</li>
    </ul>
  </li>
  <li>Obsess about promotion
    <ul>
      <li>Ignoring your personal life, family, friends, and overall happiness. Your ultimate goal should be happiness; career progress is but one element of it.</li>
    </ul>
  </li>
  <li>Assume you are ready for level L+1
    <ul>
      <li>When the overwhelming majority of your peers, collaborators, and your manager repeatedly try to tell you that you aren’t. (Listen to others, read between the lines, and don’t assume you know better)</li>
    </ul>
  </li>
  <li>Do great work in some months
    <ul>
      <li>But slack off in other months. A consistent growth trajectory is key.</li>
    </ul>
  </li>
</ul>

<h3 id="org">Org</h3>

<ul>
  <li>Keep working hard
    <ul>
      <li>But on the “wrong” team 🤷</li>
      <li>Wrong is subjective, but if you’ve been on a team for &gt; 12-18 months and have not had any major launch or impactful delivery to show for it, that’s not good for your promo case.</li>
    </ul>
  </li>
  <li>Be ready for promo
    <ul>
      <li>But in an org full of other strong performers, who are all ready for promo in the same cycle.</li>
    </ul>
  </li>
</ul>

<p><span class="swash"></span></p>

<p>So you followed all the advice in this doc, applied for promo, and failed. Now here’s how you might get rejected again the next time.</p>

<h3 id="got-rejected">Got rejected?</h3>

<ul>
  <li>Fail to get promoted
    <ul>
      <li>And be sad about it for more than 2 weeks. Take some time to get over it, then get over it.</li>
    </ul>
  </li>
  <li>Go up a second time for promotion after a rejection
    <ul>
      <li>But fail to directly address the areas for development pointed out by the previous committee/session</li>
    </ul>
  </li>
</ul>

<h3 id="or-if-eventually-you-do-get-promoted">Or, if eventually you do get promoted…</h3>

<p>Congratulations!</p>

<p>Though, keep in mind: If you do get promoted when you are not fully ready, you risk not being able to meet your expectations at the next level. This can have much more serious consequences than if you had not been promoted in the first place (and thus continued to be measured against previous-level expectations).</p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[A Googler’s perspective on why so many promotion attempts fail, and what you can do about it.]]></summary></entry><entry><title type="html">Make new features discoverable without annoying your users</title><link href="https://manas.tungare.name/blog/make-new-features-discoverable-without-annoying-your-users" rel="alternate" type="text/html" title="Make new features discoverable without annoying your users" /><published>2016-09-16T00:00:00+00:00</published><updated>2016-09-16T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/make-new-features-discoverable-without-annoying-your-users</id><content type="html" xml:base="https://manas.tungare.name/blog/make-new-features-discoverable-without-annoying-your-users"><![CDATA[<p>Developers are faced with a choice: we need to make our users aware of new
features that have been built since the last release. But we don’t want to get
in the way of users trying to use our apps, so it has to be done without
breaking their flow.</p>

<p>Not every version is a major release, so we also don’t want to show a large
in-your-face message every single time the app is updated.</p>

<p>Some features are important enough that if existing users found out about them,
they might start using the app more. But if they’ve abandoned the app
(installed, but not using it), they won’t even find out about the new features
if they never opened the app.</p>

<p>This is where having a <a href="/blog/manage-versioncode-and-versionname-with-gradle">well-defined version
structure</a>
comes into play. If you haven’t yet read my post about managing your Android
build’s major, minor and patch versions individually, now is a good time to do
that! This post relies on that structured versioning approach.</p>

<p>Here’s how my app does it. People seem to like it, we’ve heard zero complaints
about any of it, and we see bumps in usage when highlighting new features.</p>

<ul>
  <li><strong>For a patch release, do nothing.</strong> Ideally, a revision release (or patch
release) only fixes existing bugs &amp; makes unimportant improvements to the
overall UX, so let those speak for themselves.</li>
  <li><strong>For a minor release, show an in-app snackbar</strong> with a button to open the full
changelog. Never open the full changelog, which interrupts the user’s flow.</li>
  <li><strong>For a major release, show a low-priority notification</strong> with no vibration and
no sound, showing a brief snippet for the new features.</li>
</ul>

<p>Here’s the code:</p>

<p>Android can tell us when our app gets updated. This is the perfect callback to
hook into.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public class PackageUpdateReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent received) {
    if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(
        receivedIntent.getAction())) {
      onUpgraded(context, BuildConfig.VERSION_CODE);
    }
  }
}
</code></pre></div></div>

<p>Make sure your AndroidManifest.xml is set up for receiving these broadcasts, by
subscribing to the system standard broadcasts for
<strong>android.intent.action.MY_PACKAGE_REPLACED.</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;receiver android:name=".PackageUpdateReceiver"&gt;
  &lt;intent-filter android:priority="1000"&gt;
    &lt;action android:name="android.intent.action.MY_PACKAGE_REPLACED"/&gt;
  &lt;/intent-filter&gt;
&lt;/receiver&gt;
</code></pre></div></div>

<p>The first thing we do in <strong>onUpgraded</strong> is to check if we should show a
notification.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>private void onUpgraded(Context context, int versionCode) {
  if (shouldNotifyOnUpgraded(context, versionCode)) {
    notifyOnUpgraded(context, versionCode);
  }
  maybeShowSnackbarOnNextAppLaunch(context, versionCode);
  // This must be the last line in the method; previous methods 
  // use this value to determine the “old” version installed.
  Prefs.edit(context).putInt(Prefs.INSTALLED_VERSION,
      versionCode).apply();
}
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">showNotifyOnUpgraded</code></strong> checks if the major version of the app has changed from
the last one recorded. Pretty straightforward.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/**
 * Notify the user if and only if the major version (first component
 * of major.minor.patch version) is strictly greater than the 
 * current version. This ensures that we notify the user no more
 * than once every major version.
 */
private boolean shouldNotifyOnUpgraded(Context context, 
                                       int versionCode) {
  // If user has opted out of notifications, never notify.
  if (!Prefs.get(context).getBoolean(
      Prefs.NOTIFY_ON_UPGRADES, true /* defaultValue */)) {
    return false;
  }
  int lastMajorVersion = 
      Prefs.get(context).getInt(Prefs.INSTALLED_VERSION, 0) / 10000;
  int currentMajorVersion = versionCode / 10000;
  return currentMajorVersion &gt; lastMajorVersion;
}
</code></pre></div></div>

<p>Now comes the actual notification. In the interest of clarity, we leave out the
bits where we use an image loader to grab a bitmap off the main thread, and
highlight only the call to the <em>NotificationManager</em>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>private void notifyOnUpgraded(Context context, int versionCode) {
  PendingIntent openedPendingIntent = PendingIntent.getBroadcast(
      context, 0, new Intent(
          Constants.Actions.ACTION_UPGRADE_NOTIFICATION_OPENED),
          PendingIntent.FLAG_UPDATE_CURRENT);
  PendingIntent optOutPendingIntent = PendingIntent.getBroadcast(
      context, 0, new Intent(
          Constants.Actions.ACTION_UPGRADE_NOTIFICATION_OPT_OUT),
          PendingIntent.FLAG_UPDATE_CURRENT);
  NotificationCompat.Builder upgradeNotification = 
     new NotificationCompat.Builder(context)
          .setSmallIcon(R.drawable.ic_whatshot_grey600_24dp)
          .setLargeIcon(bitmap)  // Code left out for clarity.
          .setAutoCancel(true)
          .setContentTitle(context.getString(R.string.new_features))
          .setContentText(context.getString(R.string.app_version))
          .setContentIntent(openedPendingIntent)
          .setPriority(Notification.PRIORITY_DEFAULT)
          .addAction(new NotificationCompat.Action(
              R.drawable.ic_whatshot_grey600_24dp,
              context.getString(R.string.whats_new),
              openedPendingIntent))
          .addAction(new NotificationCompat.Action(
              R.drawable.ic_remove_circle_outline_grey600_24dp,
              context.getString(R.string.dont_show), 
              optOutPendingIntent));
  NotificationManager notificationManager = (NotificationManager)
      context.getSystemService(Context.NOTIFICATION_SERVICE);
  notificationManager.notify(UPGRADE_NOTIFICATION_ID,
      upgradeNotification.build());
}
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@Override
public void onReceive(Context context, Intent receivedIntent) {
  if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(
      receivedIntent.getAction())) {
    onUpgraded(context, BuildConfig.VERSION_CODE);

  } else if (Constants.Actions.ACTION_UPGRADE_NOTIFICATION_OPENED
      .equals(receivedIntent.getAction())) {
    cancelNotification(context);

    Intent showWhatsNewIntent = new Intent(
        context, MainActivity.class);
    showWhatsNewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    showWhatsNewIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
    showWhatsNewIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
    context.startActivity(showWhatsNewIntent);

  } else if (Constants.Actions.ACTION_UPGRADE_NOTIFICATION_OPT_OUT
      .equals(receivedIntent.getAction())) {
    cancelNotification(context);

    Prefs.edit(context).putBoolean(
        Prefs.NOTIFY_ON_UPGRADES, false).apply();
    Toast.makeText(context, 
        R.string.upgrade_notification_turned_off, Toast.LENGTH_LONG)
        .show();

    Intent showSettingsIntent = new Intent(
        Constants.Actions.ACTION_SHOW_SETTINGS, null /* uri */,
        context, MainActivity.class);
    showSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    context.startActivity(showSettingsIntent);
  }
}
</code></pre></div></div>

<p>Finally, for changes to the major version or the minor version, we will show a
snackbar inside the app. For that, we don’t do any actual processing inside
<em>PackageUpdateReceiver</em>; we simply set a <em>SharedPreference</em> that the main
<em>Activity</em> (or <em>Fragment</em>) reads. If true, it shows a snackbar and then clears
the bit.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/**
 * Set up a SharedPreference for whether to show a “What’s New”
 * snackbar the next time the app is launched. This is done for all
 * minor versions, which, by definition, have a user-noticeable
 * feature. This is not shown for patch versions (e.g. going from
 * 1.2.7 to 1.2.8) because patch releases are bug-fix-only or minor
 * enhancements only.
 */
private void maybeShowSnackbarOnNextAppLaunch(Context context, 
                                              int versionCode) {
  int lastMajorMinorVersion =
      Prefs.get(context).getInt(Prefs.INSTALLED_VERSION, 0) / 100;
  int currentMajorMinorVersion = versionCode / 100;
  if (currentMajorMinorVersion &gt; lastMajorMinorVersion) {
    Prefs.edit(context).putBoolean(
        Prefs.SHOW_WHATS_NEW_ON_NEXT_LAUNCH, true).commit();
  }
}
</code></pre></div></div>

<p>Throughout this example, <em>Prefs</em> is a singleton convenience wrapper for the
<em>SharedPreferences</em> class. This is what it looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public class Prefs {
  private static SharedPreferences preferences;

  private Prefs() {
  }

  /**
   * @return A read-only instance of this app’s default
   * {@code SharedPreferences}.
   */
  public static SharedPreferences get(Context context) {
    if (preferences == null) {
      preferences =
          PreferenceManager.getDefaultSharedPreferences(context);
    }
    return preferences;
  }

  public static SharedPreferences.Editor edit(Context context) {
    return get(context).edit();
  }
}
</code></pre></div></div>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[Developers are faced with a choice: we need to make our users aware of new features that have been built since the last release. But we don’t want to get in the way of users trying to use our apps, so it has to be done without breaking their flow.]]></summary></entry><entry><title type="html">Manage your Android app’s versionCode &amp;amp; versionName with Gradle</title><link href="https://manas.tungare.name/blog/manage-versioncode-and-versionname-with-gradle" rel="alternate" type="text/html" title="Manage your Android app’s versionCode &amp;amp; versionName with Gradle" /><published>2015-12-28T00:00:00+00:00</published><updated>2015-12-28T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/manage-versioncode-and-versionname-with-gradle</id><content type="html" xml:base="https://manas.tungare.name/blog/manage-versioncode-and-versionname-with-gradle"><![CDATA[<p>Don’t repeat yourself — specify app version metadata just once.
Gradle, which is now Android Studio’s default and recommended build system, can
help you automate many tasks that other developers might be doing manually.</p>

<blockquote>
  <p>Update: I posted a new article which uses the setup from this article to
<a href="/blog/make-new-features-discoverable-without-annoying-your-users">inform your users whenever an app is updated with new features</a>.</p>
</blockquote>

<p>As you know, every Android app must declare an app’s
<a href="http://developer.android.com/guide/topics/manifest/manifest-element.html#vcode">versionCode</a>
(a monotonically increasing integer for each version of the app), and
<a href="http://developer.android.com/guide/topics/manifest/manifest-element.html#vname">versionName</a>
(a text description which can really be anything, but is usually a
human-readable version string of the form x.y.z (major version, minor version,
patch level). An app’s versionName is usually shown within the app in the About
screen, and in other places, such as when sending a bug report. Here’s how you
can specify everything in exactly one place and use it everywhere needed.</p>

<h4 id="define-individual-components-of-the-version-number">Define individual components of the version number</h4>

<p>Start off with defining individual components of the version number—major
version, minor version and patch level—as separate Gradle variables. This is the
one place you’d update the numbers for every version you release.</p>

<p><code class="language-plaintext highlighter-rouge">module/app.gradle:</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def versionMajor = 2
def versionMinor = 4
def versionPatch = 1
</code></pre></div></div>

<h4 id="compute-values-for-versioncode-and-versionname">Compute values for versionCode and versionName</h4>

<p>Then have Gradle compute the versionCode from these three components as a
place-value-based number. The versionName is a straightforward string
concatenation of the three components.</p>

<p><code class="language-plaintext highlighter-rouge">module/app.gradle:</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>android {
  defaultConfig {
    applicationId “com.you.app”
    versionCode versionMajor * 10000 
                + versionMinor * 100 
                + versionPatch
    versionName “${versionMajor}.${versionMinor}.${versionPatch}”
  }
}
</code></pre></div></div>

<p>This will, for example, assign a version code of 20401 for an app with version
2.4.1. That gives you the possibility of 100 minor versions for every major
version, and 100 patch levels for every minor version. Seems enough, right?</p>

<h4 id="generate-a-string-resource">Generate a string resource</h4>

<p>Next, have Gradle generate a string resource for you automatically, which you
can use in your about app’s XML layouts &amp; Java code (as <code class="language-plaintext highlighter-rouge">@string/app_version</code> &amp;
<code class="language-plaintext highlighter-rouge">getResources().getString(R.string.app_version)</code> respectively).</p>

<p><code class="language-plaintext highlighter-rouge">module/app.gradle:</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>buildTypes {
  debug {
    versionNameSuffix ".debug"
    resValue "string", "app_version",
             "${defaultConfig.versionName}${versionNameSuffix}"
  }
  release {
    resValue “string”, “app_version”,
             “${defaultConfig.versionName}”
  }
}
</code></pre></div></div>

<h4 id="identify-your-debug-releases">Identify your debug releases</h4>

<p>The code snippet above adds <strong><code class="language-plaintext highlighter-rouge">.debug</code></strong> to the versionName of all debug
releases, so if you see one of these in your logs, you’ll know this wasn’t a
release that your users saw — phew! The resource value isn’t automatically
updated when we add a versionNameSuffix, so we handle that separately for the
debug build type.</p>

<h4 id="check-your-generated-androidmanifestxml">Check your generated <code class="language-plaintext highlighter-rouge">AndroidManifest.xml</code></h4>

<p>Now if you look at your generated AndroidManifest.xml, you’ll see that it has
the correct auto-generated versionCode and versionName. Note, this is under the
<code class="language-plaintext highlighter-rouge">build/</code> directory, not the one in your <code class="language-plaintext highlighter-rouge">app/</code> directory.</p>

<p><code class="language-plaintext highlighter-rouge">build/intermediates/full/debug/AndroidManifest.xml:</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;manifest xmlns:android=”…"
   package=”com.you.app”
   android:versionCode=”20401"
   android:versionName=”2.4.1.debug”&gt;
</code></pre></div></div>

<h4 id="show-the-version-in-your-about-screen">Show the version in your About screen</h4>

<p>If you use a Preference-based About screen, simply use the generated string as
the preference value.</p>

<p><code class="language-plaintext highlighter-rouge">about.xml:</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;Preference
    android:summary="@string/whats_new"
    android:title="@string/app_version" /&gt;
</code></pre></div></div>

<h4 id="use-it-everywhere-you-need-to-identify-a-specific-release">Use it everywhere you need to identify a specific release.</h4>

<p>You can use it in the subject of your “Send feedback” email, or as part of your
analytics reporting, or in your app upgrade tutorials. And if you need to read
it into a Java variable, that’s as straightforward as
<code class="language-plaintext highlighter-rouge">getResources().getString(R.string.app_version)</code>.</p>

<p><code class="language-plaintext highlighter-rouge">FeedbackUtils.java:</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>static void sendFeedbackEmail(Context context) {
  Intent emailIntent = new Intent(Intent.ACTION_SENDTO,
      Uri.fromParts(“mailto”, "support@example.com", null));
  emailIntent.putExtra(Intent.EXTRA_SUBJECT, 
      context.getString(R.string.email_title, 
          context.getString(R.string.app_version)));
  emailIntent.putExtra(Intent.EXTRA_TEXT,
      context.getString(R.string.email_body));
  context.startActivity(Intent.createChooser(emailIntent,
      context.getString(R.string.send_feedback)));
}
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">res/values/strings.xml:</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;resources&gt;
  &lt;string name="email_title"&gt;Feedback about App %1$s&lt;/string&gt;
  &lt;string name="email_body"&gt;Hello!&lt;/string&gt;
  &lt;string name="send_feedback"&gt;Send feedback&lt;/string&gt;
&lt;/resources&gt;
</code></pre></div></div>

<h4 id="why-not-buildconfig">Why not <code class="language-plaintext highlighter-rouge">BuildConfig</code>?</h4>

<p>There is another approach that recommends putting these fields in BuildConfig,
which makes it available to your code as a generated Java source file,
BuildConfig.java.The problem is, there is no associated XML string, so using it
in layouts, menus, preferences, and other strings is not straightforward. By
exporting it as a string resource, you gain the flexibility to use it either as
a resource or as a Java string.</p>

<h4 id="automate-everything-that-is-not-primary-to-the-task-at-hand">Automate everything that is not primary to the task at hand</h4>

<p>And spend that saved time on new features!</p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[Don’t repeat yourself — specify app version metadata just once. Gradle, which is now Android Studio’s default and recommended build system, can help you automate many tasks that other developers might be doing manually.]]></summary></entry><entry><title type="html">Implement Free &amp;amp; Paid Apps with in-app purchases instead of as two apps</title><link href="https://manas.tungare.name/blog/implement-free-and-paid-apps-using-in-app-purchases" rel="alternate" type="text/html" title="Implement Free &amp;amp; Paid Apps with in-app purchases instead of as two apps" /><published>2015-09-09T00:00:00+00:00</published><updated>2015-09-09T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/implement-free-and-paid-apps-using-in-app-purchases</id><content type="html" xml:base="https://manas.tungare.name/blog/implement-free-and-paid-apps-using-in-app-purchases"><![CDATA[<p>Offering paid upgrades to users of a free app can be implemented either as two separate apps
(Free and Paid), or within a single app that is free to download with in-app upgrades for the
premium features. I have found and heard from fellow developers that a single app with in-app
purchases offers multiple advantages over the two-app approach, for both developers and users.</p>

<h3 id="a-seamless-user-experience-when-upgrading">A seamless user experience when upgrading</h3>

<p>When a user decides to buy your app, they can simply tap a button and be upgraded instantly.
With two separate apps, they must uninstall the free one and then download the paid one.
Whatever progress they may have made so far (saved data, trained models) will be lost.</p>

<h3 id="preferences-are-maintained-even-after-upgrading">Preferences are maintained even after upgrading</h3>

<p>No additional work is necessary for a user’s preferences to be shared between the free app and
the paid app, because it’s the same app.</p>

<p>With the two-app approach, users either have to start from scratch, losing all their settings,
or you, the developer, have to take additional steps to ensure that preferences are correctly
migrated. And even then, the experience is not seamless, since users now have to authenticate
or perform import/export steps themselves.</p>

<p>With a single app, this is a non-issue, and offers the smoothest user experience. If you have a
server-less app, this will save you the effort of having to stand up any kind of server-side support.</p>

<h3 id="launcher-clutter--icon-placement">Launcher clutter &amp; icon placement</h3>

<p>Some apps require you to have both the free and the paid version installed on the device for the
whole system to work correctly in paid mode. This means that both apps appear separately in the
launcher, most likely next to each other. Tapping on the wrong one elicits a message to the tune
of “hey, this is just the key; go open the other one instead.” This is not a good user experience.</p>

<p>A single app provides a single entry point, and eliminates any user confusion stemming from
duplicate apps. During the upgrade, if users take the time to place your app’s icon on their
home screen, they no longer have to remove and replace that icon with the paid app’s icon.
Everything just works.</p>

<h3 id="maintaining-two-app-variants-is-hard">Maintaining two app variants is hard</h3>

<p>Although you can configure Gradle to build multiple flavors of your app, you have to take extra
engineering steps to ensure that both build and work correctly as expected. Testing efforts are
duplicated (or at least 1.5x) with two separate APKs.</p>

<h3 id="bad-for-seo-search-engine-optimization--aso-app-store-optimization">Bad for SEO (Search Engine Optimization) &amp; ASO (App Store Optimization)</h3>

<p>There are now two listings on the Play Store, splitting the SEO juice you’re getting. Reviews,
reports, and inbound links to your app get divided between the two versions.</p>

<h3 id="easier-to-pirate-a-full-featured-apk">Easier to pirate a full-featured APK</h3>

<p>If the paid version offers all the features unlocked, users may find it easier to pirate that
APK elsewhere and offer it to others unlicensed. Since in-app purchases are activated within
the app, and it’s possible to require license verification with your own server, those are
harder to pirate than full-featured APKs.</p>

<p>Also, IAPs cannot be refunded on Google Play (unlike purchased apps, which can be refunded
within a 15-minute window for full price). This can cut down on piracy from users who would
otherwise download the paid APK (after paying for it), then copy the APK off the device,
refund it, and then reinstall from the copied APK.</p>

<h3 id="multiple-price-points-possible-with-iaps">Multiple price points possible with IAPs</h3>

<p>Any more than two separate apps and the multiple-app model fails to scale well. But with a
single-app-with-IAPs, it’s easy to set up per-feature prices in addition to a strict dichotomy
between Free and Paid. Some features might be worth a lot more, such that even most Paid users
would not care about them. It’s possible to charge only those users who are likely to need that
feature, while keeping the rest of the app free/cheaper for everyone else.</p>

<h3 id="offer-value-not-annoyance-removal">Offer value, not annoyance removal</h3>

<p>Don’t charge your users simply to remove an annoyance (such as ads). Charge them to use a feature
they would love using (and love to pay for using). Offer a time-limited trial of the paid version
so they’ll know what to expect. The code is in there already (since it’s a single app!), so just
unlock it for the first 24 or 48 or 72 hours, and let them get a taste of what the full-featured
version looks like. Keep them aware that this is a time-limited trial, and offer them incentives
to upgrade even before the trial period is over. And then, if the user chooses not to purchase,
just lock the feature up beyond the trial period.</p>

<h3 id="whats-your-experience-so-far">What’s your experience so far?</h3>

<p>If you’ve experimented with these issues, I’d love to hear what your experience has been! What
do your users say about your app? What hypotheses have your analytics confirmed / failed to confirm?</p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[Offering paid upgrades to users of a free app can be implemented either as two separate apps (Free and Paid), or within a single app that is free to download with in-app upgrades for the premium features. I have found and heard from fellow developers that a single app with in-app purchases offers multiple advantages over the two-app approach, for both developers and users.]]></summary></entry><entry><title type="html">It’s neither possible nor desirable for changelogs to be bland lists of features</title><link href="https://manas.tungare.name/blog/changelogs-cannot-be-bland-lists-of-features" rel="alternate" type="text/html" title="It’s neither possible nor desirable for changelogs to be bland lists of features" /><published>2015-08-29T00:00:00+00:00</published><updated>2015-08-29T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/changelogs-cannot-be-bland-lists-of-features</id><content type="html" xml:base="https://manas.tungare.name/blog/changelogs-cannot-be-bland-lists-of-features"><![CDATA[<p>Changelogs can be straightforward for simple apps that have no server-side component, but for those
where most features are turned on/off or tweaked heavily server-side, publishing a client app’s
changelog will be either incorrect, or useless, or both.</p>

<p>TheNextWeb <a href="http://thenextweb.com/insider/2015/08/29/app-changelogs-should-be-useful-not-easter-eggs-or-boilerplates/">argues</a>
that changelogs for each client update should be “useful”, but that’s sometimes neither possible,
nor desirable.</p>

<p>There are several reasons for this.</p>

<h3 id="the-release-train-model">The release train model</h3>

<p>Many large app publishers follow a release train model. They don’t wait to push app updates until a
new feature is ready. They’ll push every two weeks (as Facebook say in their release notes), or
once a month, or at some other predetermined interval.</p>

<p>This ensures that important bug fixes unrelated to upcoming features are not held up for
indeterminate amounts of time. It makes sure that delays in one feature do not impact other
unrelated features. If a feature is ready for a particular release, it gets enabled in the shipping
release; if not, it gets cut from that release and can be enabled in the next one.</p>

<p>In such a release model, it’s hard to write accurate release notes and keep them accurate throughout
the release testing phase. It risks leaking confidential features before they’re ready. It risks
raising user expectations and then shattering them if a feature does not make the cut.</p>

<h3 id="server-side-feature-rollout">Server-side feature rollout</h3>

<p>Some features in large apps are controlled server-side. Client apps are published according to each
platform’s review delay timelines. iOS apps need to leave time for a review by Apple. Android apps
need time for review by Google. Coordinating all of these to launch a feature at a specific time is
tricky.</p>

<p>New features are often rolled out in stages. When feature rollout is controlled server-side, it can
be enabled for 1% of users first, then 10% and so on, up to 100%, across Android, iOS, and Web.
Managing this rollout entirely server-side is much easier than trying to do it client-side, e.g.
through individual app store staged rollouts.</p>

<p>For the 90% of users who have not yet seen a feature enabled when the rollout is at 10%, the
changelog will be flat out incorrect. It’s best to leave it out and announce the feature in-app
when it’s enabled for that specific user.</p>

<h3 id="ab-experiments">A/B experiments</h3>

<p>Even after a feature has been rolled out widely, there will be tweaks made between individual users.
If you start listing all the combinations of A/B experiments going on at any given time, the
combinatorial explosion will make the changelog impossible to read or parse.</p>

<p>Some A/B experiments must be kept secret from the users; if you reveal too much about what is being
tested, you tend to skew the results of the experiment. Such experiments need to be kept away from
media attention as well, lest a subset of potential users in affected buckets go on to read about
them. There is nothing nefarious in this; just proper experiment design that requires that
participants not be aware of what is being evaluated.</p>

<h3 id="upgrading-across-multiple-versions">Upgrading across multiple versions</h3>

<p>What should a changelog say when you go from version 1.0 to version 6.0 in one fell swoop? That
sometimes happens, if app update checking mechanisms have been misconfigured, or an app requires
manual approval for updates.</p>

<p>In such cases, what should a changelog meaningfully say? Should it aggregate all changelogs between
1.0 and 6.0? Should it show only the delta between 5.9 and 6.0? It’s just not feasible or practical
to list exactly the set of things that are new in the most recent update because you may never know
the baseline from which the user is upgrading.</p>

<h3 id="humor-is-part-of-branding">Humor is part of branding</h3>

<p>Changelogs that make me chuckle are a nice touch on otherwise bland days. Some brands explicitly
espouse humor as part of their branding. Including playful funny text (well, funny to some,
perhaps) is their way of strengthening their brand, of grabbing the attention of new users, and
subtle advertising. What’s the harm in that?</p>

<h3 id="instead-prefer-in-app-promos-or-mentions">Instead, prefer in-app promos or mentions</h3>

<p>Not all users will read changelogs, so even if they contain useful information, it will likely be
skipped. If you want to convey to your users when a new feature is ready for them, or want to
highlight how to use it, prefer to use in-app mentions or promos. A quick text field with a line or
two of text, or a bubble with a link, can be far more informative than putting that same
information in a changelog.</p>

<p>Of course, make sure you don’t interrupt the user flow or annoy them with these promos.</p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[Changelogs can be straightforward for simple apps that have no server-side component, but for those where most features are turned on/off or tweaked heavily server-side, publishing a client app’s changelog will be either incorrect, or useless, or both.]]></summary></entry><entry><title type="html">Geotagging photos from an SLR using a second camera’s GPS logger</title><link href="https://manas.tungare.name/blog/geotagging-photos-from-an-slr-using-a-second-cameras-gps-logger" rel="alternate" type="text/html" title="Geotagging photos from an SLR using a second camera’s GPS logger" /><published>2012-08-12T00:00:00+00:00</published><updated>2012-08-12T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/geotagging-photos-from-an-slr-using-a-second-cameras-gps-logger</id><content type="html" xml:base="https://manas.tungare.name/blog/geotagging-photos-from-an-slr-using-a-second-cameras-gps-logger"><![CDATA[<p>Every place has its memories, and every memory its place. Having information about places embedded
in photos and videos serves to link the two. We’re in an age where capturing every bit of metadata
is certain to lead to interesting applications in the future.  Here’s a set of tips and links to
software that allows embedding geographic information into photos.</p>

<p>If you have a Canon 5D Mark II, and looking to geotag your photos, here’s a solution cheaper than
the official Canon accessory — it happens to be a Canon compact camera.</p>

<p>Every place has its memories, and every memory its place. Having information about places embedded
in photos and videos serves to link the two. We’re in an age where capturing every bit of metadata
is certain to lead to interesting applications in the future. I’ve been
<a href="https://en.wikipedia.org/wiki/Geotagged_photograph">geotagging my photos</a>
since 2006 or so using a <a href="https://sites.garmin.com/etrex/">Garmin eTrex series
GPS logger</a>. These days, many cameras have built-in GPS hardware
and loggers which make the job trivial — just turn the option on. But not many SLRs have them yet.
Canon sells
<a href="https://www.usa.canon.com/cusa/consumer/products/cameras/consumer_cameras_wft/wireless_file_transmitter_wft_e4_ii_a">a kit for the 5D Mark II</a>
which is insanely expensive. (US$699 retail — crazy, right?)</p>

<p>My solution: I recently bought a
<a href="https://www.amazon.com/gp/product/B0075SUKIC/ref=as_li_tf_tl?ie=UTF8&amp;camp=1789&amp;creative=9325&amp;creativeASIN=B0075SUKIC&amp;linkCode=as2&amp;tag=techkie05-20">Canon PowerShot D20</a>,
which is an awesome camera in its own right. It has a GPS logger that can be turned on and left on
even when the (rest of the) camera is off. And its effect on battery life is minimal enough that I
could get about two–three days’ worth of photos &amp; logs on a single charge. Of course, you could use
any</p>

<p>Next, I used
<a href="https://www.gpsvisualizer.com/convert_input?convert_format=gpx">GPS Visualizer’s Convert to GPX tool</a>
to convert from Canon’s GPS log
format to GPX, a fairly standard format. I tried <a href="https://www.gpsbabel.org/">GPSBabel</a>, but I
couldn’t find the right format to pick as the input format (despite a lot of Googling), so it never
worked for me.</p>

<p><img src="/static/blog/2012/08/GPS-Visualizer.png" alt="" title="GPS Visualizer" class="img-hero" /></p>

<p>After that, I used <a href="https://www.earlyinnovations.com/gpsphotolinker/">GPSPhotoLinker</a>, a great tool
for Mac OS X, that reads tracks and waypoints in GPX format, and embeds that info into a set of
photos as EXIF tags.</p>

<p><img src="/static/blog/2012/08/GPSPhotoLinker.png" alt="" title="GPSPhotoLinker" class="img-hero" /></p>

<p>Once thus embedded, almost all photo tools that are geo-aware can make use of this info. You can see
them on <a href="https://www.flickr.com/map/">a map on Flickr</a>,
view <a href="https://www.panoramio.com/map/">related photos on Panoramio</a>, and what have you.</p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[Every place has its memories, and every memory its place. Having information about places embedded in photos and videos serves to link the two. We’re in an age where capturing every bit of metadata is certain to lead to interesting applications in the future. Here’s a set of tips and links to software that allows embedding geographic information into photos.]]></summary></entry><entry><title type="html">URL Design Sins: 16 things that don’t belong in URLs</title><link href="https://manas.tungare.name/blog/url-design-sins-16-things-that-dont-belong-in-urls" rel="alternate" type="text/html" title="URL Design Sins: 16 things that don’t belong in URLs" /><published>2011-03-05T00:00:00+00:00</published><updated>2011-03-05T00:00:00+00:00</updated><id>https://manas.tungare.name/blog/url-design-sins-16-things-that-dont-belong-in-urls</id><content type="html" xml:base="https://manas.tungare.name/blog/url-design-sins-16-things-that-dont-belong-in-urls"><![CDATA[<p>Much has been said for a
<a href="https://www.useit.com/alertbox/990321.html">long</a>
<a href="https://www.w3.org/Provider/Style/URI">time</a>
about making your URLs easy to use, remember, type, hack,
and spread virally. There is still no dearth of ugly URLs all over the Web. A few very popular
content management systems also engage in dirty URL practices, and it’s a shame. To aid you in
cleaning up your URLs, here’s a list of specific things that do not belong in a URL.</p>

<ol>
  <li>
    <p><strong>www.</strong> We’ve spent enough time with the World Wide Web to know that web pages reside on the
WWW. Adding those four characters to the beginning of every single URL not only requires users to
type them in every time, but also requires 4 extra bytes in every single database that stores URLs.
Think for a moment how many bytes that would be. <a href="https://no-www.org/">Get rid of them!</a> And after
you do that, <a href="https://www.mattcutts.com/blog/seo-advice-url-canonicalization/">make sure all your <code class="language-plaintext highlighter-rouge">www.</code> URLs redirect to the non-<code class="language-plaintext highlighter-rouge">www.</code> version</a>.</p>
  </li>
  <li>
    <p><strong>Port numbers.</strong> Unless your site is under test, there is no valid reason for hosting it on a
non-default port (i.e., a port other than 80.) Apache on Mac OS X has a performance cache that runs
on port 16080, and makes every URL of the form <code class="language-plaintext highlighter-rouge">https://your-site.com:16080/</code>. Unless you find a
mechanism to run the performance cache on port 80, it is a good idea to dump the cache. It’s not
worth the confusing URL (to most users, if not to you.) Standard well-known port numbers are there
for a reason.</p>
  </li>
  <li>
    <p><strong>Index filenames.</strong> Filenames such as <code class="language-plaintext highlighter-rouge">index.php</code> and <code class="language-plaintext highlighter-rouge">default.asp</code> do not give us any more
information than the rest of the URL. Drop them.</p>
  </li>
  <li>
    <p><strong>Details of the server-side technology.</strong> Your users don’t need to know what software you’re
running behind the scenes. They couldn’t care less about whether your pages are .php, .jsp, .aspx
or .do. It’s best to configure your server to hide these extensions, and then <a href="https://www.w3.org/TR/2003/NOTE-chips-20030128/#gl3">make sure none of
your URLs contain them</a>.</p>
  </li>
  <li>
    <p><strong>Special directories for special scripts.</strong> You no longer need to place your scripts in a
<code class="language-plaintext highlighter-rouge">cgi-bin</code>. Get rid of that directory and any others like that. If your server requires you to do
something like that, either find a way to configure it correctly, or
<a href="https://apache.org/">upgrade to one</a> that will let you do that.</p>
  </li>
  <li>
    <p><strong>Document maintainers’ names.</strong> Often, when each document has an assigned maintainer for some
duration of time, those documents end up being in that particular person’s web space. Later, when
the maintainer moves on or someone else takes over the maintenance, you’re left with a different
URL than what you started with. To avoid this, it’s best to categorize documents by topic and
subject instead of under <code class="language-plaintext highlighter-rouge">~username/document.html</code>.</p>
  </li>
  <li>
    <p><strong>Internal database IDs.</strong> Sure, your content management system needs those IDs to locate your
content, but your users don’t need to know. If it takes an extra database lookup to get the ID from
the URL, then so be it.</p>
  </li>
  <li>
    <p><strong>CMS Module Names.</strong> Use a CMS that is intelligent enough to render a page without needing all
sorts of information stored in the URL. Joomla is particularly notorious at this. What does this
URL tell you about where it will take you?</p>

    <blockquote>
      <p><a href="https://www.joomla.org/content/section/1/74/">https://www.joomla.org/content/section/1/74/</a></p>
    </blockquote>

    <p>Now what if it were:</p>

    <blockquote>
      <p><a href="https://joomla.org/news">https://joomla.org/news</a></p>
    </blockquote>
  </li>
  <li>
    <p><strong>MiXeD-CaSe NaMeS.</strong> Don’t confuse your users
by-Mixing-Upper-case-and-Lower-case-Characters-in-the-URL. Stick to lower-case letters, and don’t
make them guess. If your user actually types in a URL in mixed case, normalize it on the server and
serve the appropriate case.</p>
  </li>
  <li>
    <p><strong>Random gunk.</strong> Unless you are a URL-compressor service such as
<a href="https://tinyurl.com/">Tiny URL</a> or <a href="https://snipurl.com/">SnipURL</a>,
forget using random characters in your URL. Nobody wants to visit
<code class="language-plaintext highlighter-rouge">https://yourdomain.com/WijHyYQnVPWNs</code> and guess what it might lead to.</p>
  </li>
  <li>
    <p><strong>Session IDs.</strong> Make sure no user-session-specific identifiers end up in your URLs. This makes
sure that users can pass on URLs to other users via email or IM, be able to bookmark them, and be
sure that they represent a single resource. There are
<a href="https://en.wikipedia.org/wiki/HTTP_cookie">better</a> <a href="https://www.ietf.org/rfc/rfc2109.txt">places</a>
to keep session state in.</p>
  </li>
  <li>
    <p><strong>Punctuation.</strong> Avoid punctuation that might make it difficult for people to tell others about
your wonderful site over the phone. The only punctuation you may have is a hyphen (“-“) and HTML
entities that have special meaning (e.g. ?, #, :, + and @). No underscores, commas, periods,
brackets, parentheses, braces, quotes, less-than, greater-than, equals, or pipes.</p>
  </li>
  <li>
    <p><strong>Database query details.</strong> If your web pages have even a hint of
<a href="https://www.google.com/search?q=inurl:select%20inurl:where%20inurl:%2520">database query language</a>
in the URLs, you should be on <a href="https://forums.thedailywtf.com/forums/t/8520.aspx">The Daily WTF</a>.</p>
  </li>
  <li>
    <p><strong>Repeated domain name.</strong> If the address of your web site looks like
<code class="language-plaintext highlighter-rouge">https://your-site.com/your-site/your-page.html</code>, then you should have a chat with your web hosting
provider about how to shorten it to <code class="language-plaintext highlighter-rouge">https://your-site.com/your-page.html</code>.</p>
  </li>
  <li>
    <p><strong>Inconsistent naming.</strong> If you sell several products, then make the subdirectories below each
product name exactly identical. If someone were to replace a product name by another, the rest of
the URL structure should still continue to function. In other words, strive for consistency in
naming.</p>
  </li>
  <li>
    <p><strong>Missing content at each level.</strong> When a URL is several levels deep, users should be able to
chop off parts at the end (“hack the URL”) and still be able to get to a usable page. E.g. if
you’re a news site, and if an example URL looks like:
<code class="language-plaintext highlighter-rouge">https://my-news-site.com/2008/05/21/news-story.html</code>, make sure you include a list of news
articles from 21 May 2008 at <code class="language-plaintext highlighter-rouge">https://my-news-site.com/2008/05/21/</code>, and a list of links to daily
articles for the entire month of May 2008 at <code class="language-plaintext highlighter-rouge">https://my-news-site.com/2008/05/</code>.</p>
  </li>
</ol>

<p>There are some easy technological solutions to make this work. Many of these do not require you to
change the underlying file system structure or database structure.</p>

<p>But most of this comes with discipline: there is nothing here that is technology magic. It is just
an application of common sense to a common domain (no pun intended.) Google
<a href="https://www.google.com/search?hl=en&amp;q=mod_rewrite">mod_rewrite</a> and
<a href="https://www.google.com/search?hl=en&amp;q=content+negotiation">content negotiation</a> to get started.</p>]]></content><author><name>Manas Tungare</name><email>manas@tungare.name</email></author><summary type="html"><![CDATA[Much has been said for a long time about making your URLs easy to use, remember, type, hack, and spread virally. There is still no dearth of ugly URLs all over the Web. A few very popular content management systems also engage in dirty URL practices, and it’s a shame. To aid you in cleaning up your URLs, here’s a list of specific things that do not belong in a URL.]]></summary></entry></feed>