To ask permission is to seek denial - John Doerr
So basically itβs better to not ask for permission and go ahead anyway.
Now with that quote out of the way, I can safely say that this philosophy cannot be applied in any way to the mobile worldΒ at-least.
(I've yet to see an instance where an action that requires permission is done without and there weren't any repercussions so yeah π€·)
iOS and Android both have a permission based access system for all applications. Any application that is not built by the OS developer has to explicitly granted permissions by the end user to perform the actions it needs π.
This system is basically how OSes are built for computers as well, with granular access systems embedded. The difference lies in level of granularity offered to the average user. Mobile OSes have granular permissions tied to almost every feature available. Take location, fingerprints, cameras, microphones, SD cards and so on. All of these need to be specifically granted to an app so it can use them.
Due to this, and other security measures inbuilt, mobile OSes are considered to be safer than PC OSes.
Now, iOS is built by a single company. Apple.
Every iPhone ever released, and that will ever release, will always run their custom OS which is closed source and maintained by them alone. You won't find an "OxygenOS" version of iOS that you can put onto your iPhone easily.
This makes the platform uniform. Every permission request falls into the same category, and requesting them will always be the same. Any changes made by Apple will be clearly documented for any developer to read.
This makes building for iOS pretty straightforward as there is only a small number of edge cases possible that we need to test out apps for.
Android is a whole different ballgame thought.
Almost every major Android OEM has their own flavour of Android that they slap onto their phones. Fundamentally, they all run Android, with the same set of restrictions, rules and architecture.
But they also end up adding additional restrictions, such as Battery Optimization features that vary from OEM to OEM,Β multiple permission levels that are put on top of or beside existing permissions from stock Android.
Now since these are added by OEMs for phones that are widely used, one would expect atleast some basic documentation right π€?
Right???
But no β. Most OEMs have massive discussion threads where people post issues they've faced during dev and some random user will have a solution.
No proper documentation π€¦.
And by golly is it the most painful experience for an Android developer. Especially one who's building applications that involve a lot of niche permissions and tools.
In my case, I was working on Plicly, which is a tool designed to run silently on your phone and notify you about your posture.
There were 3 particular niche permissions that I needed from a user for the app to run:
Overriding screen brightness π
One way of notifying users about precarious neck angles was to dim their screen during use. This forces a user to nudge back into the right posture and the app would reset the brightness to the original value it was at prior.
Running uninterrupted in the background π
In order to monitor posture during use, Plicly needs to run uninterrupted for hours on end. Any sort of battery optimization/process killing would render the app completely useless for its intended purpose.
Auto-start if defined by a user π
If a user scheduled start and end timings for Plicly, the app should automatically startup and perform all services. Just as it would from a manual trigger. This meant that the app should be allowed to start automatically, and that it shouldn't be blocked by any system software/restriction.
Before we continue, I'd like to mention that this post covers Android permissions only. This is due to the fact that apps like Plicly cannot run on iOS. Apple is extremely particular about applications that can run in the background, and pretty much anything apart from music players and their own apps cannot run in the background π€·.
Continuing on, let's tackle these one by one, and I'll take you through the literal hell I had to go through to find solutions that actually worked.
Overriding screen brightness
Editing screen brightness comes under the purview of the Android OS. This leads it to being categorised as System Settings. This is a very large bucket, and this permission allows apps to edit almost everything about how your device will look and feel.
Quite a general permission if you ask me, but I can't quite make a statement against why it's categorised into such a large permission group since it acts like a duck, quacks like a duck, walks like a duck, so is a duck.
This permission is the easiest to grant on all devices, as it does not have any customisation done on top by OEMs. And that is:
val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
context.startActivity(intent)
The two additional flags added are specific to how I wanted the activity to start, and how back navigation should work.
That's it!
Not gonna lie iOS makes me quite jealous as all permissions are this straightforward there, but then again you don't have any flexibility with what an app can do on iOS as well.
Running uninterrupted in the background
The USP of Plicly is that it runs silently in the background, irrespective of what the user is up to. It monitors your posture without using any invasive/monitoring sensors such as your front camera and notifies you, the user, in real time about how your posture is.
In order to achieve this , Plicly needs to run uninterrupted in the background without any system enforced shutdown or restriction. Since the service is designed to run when the user uses their phone, it could run for hours or even days on end. This meant that the app cannot be battery optimised without compromising its core function.
Before Android 12, stock Android had no specific restrictions on battery optimization. Apps could opt out of it and perform activities as needed. But certain OEMs like Samsung and OnePlus added in their own battery optimisation services.
These basically killed long running services unless they were whitelisted, which was done via:
Their own teams which whitelisted major apps like WhatsApp
Users who managed to find and navigate through their custom settings flow to disable said functionality for the app they wanted to use
Now sending users to hunt for a permission on their own is a major drop off point as it adds unnecessary complexity to it. And due to how OEMs have garbage documentation on their customisations on top of stock Android, hunting them down is another issue altogether.
So the question is how do I get users to navigate to the battery optimisation toggle pages which varies from OEM to OEM? (Sometimes even within an OEM they've multiple package names for different versions of their OSes π€¦)
The easiest way would be to use ADB and track activities as they opened. With this, I'd know what the intent contains and the package name being called. But it would mean I had to do this for every OEM, and most OEMs do not provide their Android flavours for direct downloads. Setting up an emulator for that would again be a cumbersome process. ( For how to go about the ADB way, please follow this really helpful link from SO)
I didn't want to take that approach, and instead tried to look around and find these intents on the internet.
And boy was that a long search.
After a couple of days, I finally stumbled into an answer on SO which was a goldmine. It had a list of common OEM OSes and the associated permissions. It wasn't a complete list, but after some more hunting I managed to find a comprehensive list of possible OSes and their permissions.
The next step was to put this into my app, and test it on as many devices as I could. I called up friends and family, tried to get those phones to test or send APKs to them and guide them through testing. Finally, this task was complete β .
Note: With the introduction of Android 12, Doze mode was added. This meant apps could be put to "sleep" π€, starting up only when the user opens, picks up or when plugged in to charge. This doesn't directly impact Plicly, as the service does not need to run when the app is stationary or when the screen is off, but this led to an issue where Plicly wouldn't auto-start/shut down during active hours reliably.
In order to circumvent this, I added in the necessary requests to disable Doze for Plicly. (Google states that the permission is a restricted permission, and apps can only be approved based on their individual use case. I'm currently testing to identify if this permission is a necessity for my app, and whether deferred alarms would cause the app to break.
Auto-start if defined by a user
Plicly is designed to run 24/7 365.
But ideally you wouldn't want it on all the time. Just like how we have active hours defined on our laptops and mobiles, a similar such solution was added in to the app.
We called it Posture Shift, where the app would turn itself on , everyday, and run for the specified interval of time. This meant the app was a setup-and-forget solution and would monitor your posture only during hours you wanted it to. This would make it an automatic habit as well, as a user would not have to remember to turn it on and off everyday.
In essence, what we had was an alarm β° that would trigger the app's service and launch it. I attach a payload to a system alarm, and when it triggers Plicly launches its background service.
Stock Android reliably runs a service if scheduled via an AlarmScheduler , and there are multiple options available where you can set how exact the alarm would be, set regular repeating alarms and even set approximate intervals of time (where the OS will decide when it can optimally trigger it to reduce battery consumption / CPU cycles).
But again, custom OEMs are a problem π©.
Alarms by their nature are tied to all system timing operations, and these pretty much have to run 24/7 365. Most apps use these to regularly update and schedule content for display. Alarms use CPU cycles when triggered, and when triggered during phone inactivity they consume more energy as compared to triggering when there are other active processes. This is why there are multiple options available for AlarmSchedulers for developers to pick how battery efficient πΏ they want their apps to be.
Coming to OEMs, they add further restrictions on how any process can start. Most call it "Startup App Control" or something of that sort. These add conditions as to how apps can trigger/schedule alarms.
Apps are allowed to schedule alarms only for one cycle, after which they should be opened again to reschedule. In our case, this meant that a user would have to open the app everyday, and reschedule the alarm for the next day or atleast toggle Posture Shift off and then on.Β
The amount of restrictions that most OEMs add makes my head go:
Again, OEMs have their own processes to whitelist apps which they deem are vital. Usually its their own bloatware (which are very highly essential apps π€¬), and some essential apps like WhatsApp that are automatically whitelisted.
For other apps, users again have to do the whole cycle of hunting for that specific permission and toggling it. Now this was the start of another hunt. To find the long-lost-idk-if-it-was-ever-documented-treasure-hunt-esque π΄ββ οΈ permission routing code for common OEMs.
This was by far the longest hunt for the app, as I don't even remember the source for the solution I found.
Again, this could be solved by using ADB, and the link I had shared above but then again, I did not have the devices or patience to setup those emulators.
The code I finally ended up using was this:
private fun requestAutostart(context: Context) {
val intent = Intent()
when {
Build.MANUFACTURER.equals("oppo", ignoreCase = true) -> {
Log.d(TAG, "Oppo")
try {
intent.setClassName(
"com.coloros.safecenter",
"com.coloros.safecenter.permission.startup.StartupAppListActivity"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (e: Exception) {
try {
intent.setClassName(
"com.oppo.safe",
"com.oppo.safe.permission.startup.StartupAppListActivity"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (ex: Exception) {
try {
intent.setClassName(
"com.coloros.safecenter",
"com.coloros.safecenter.startupapp.StartupAppListActivity"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (exx: Exception) {
Log.d(TAG, "Cant start anythibg")
logFailure(AUTOSTART)
}
}
}
}
Build.MANUFACTURER.equals("xiaomi", ignoreCase = true) -> {
try {
intent.component = ComponentName(
"com.miui.securitycenter",
"com.miui.permcenter.autostart.AutoStartManagementActivity"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (ex: Exception) {
Log.d(TAG, "Unable to launch MIUI activity")
logFailure(AUTOSTART)
}
}
Build.MANUFACTURER.equals("huawei", ignoreCase = true) -> {
try {
intent.component = ComponentName(
"com.huawei.systemmanager",
"com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (ex: Exception) {
Log.d(TAG, "Unable to launch huawei activity")
logFailure(AUTOSTART)
}
}
Build.MANUFACTURER.equals("vivo", ignoreCase = true) -> {
try {
intent.component = ComponentName(
"com.iqoo.secure",
"com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (e: Exception) {
try {
intent.component = ComponentName(
"com.vivo.permissionmanager",
"com.vivo.permissionmanager.activity.BgStartUpManagerActivity"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (ex: Exception) {
try {
intent.setClassName(
"com.iqoo.secure",
"com.iqoo.secure.ui.phoneoptimize.BgStartUpManager"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (exx: Exception) {
ex.printStackTrace()
logFailure(AUTOSTART)
}
}
}
}
Build.MANUFACTURER.equals("letv", ignoreCase = true) -> {
try {
intent.component = ComponentName(
"com.letv.android.letvsafe",
"com.letv.android.letvsafe.AutobootManageActivity"
)
context.startActivity(intent)
setAutostartEnabled()
} catch (ex: Exception) {
Log.d(TAG, "Unable to start for LETV");
logFailure(AUTOSTART)
}
}
Build.MANUFACTURER.equals("asus", ignoreCase = true) -> {
try {
intent.component = ComponentName(
"com.asus.mobilemanager",
"com.asus.mobilemanager.entry.FunctionActivity"
)
intent.data = android.net.Uri.parse("mobilemanager://function/entry/AutoStart")
context.startActivity(intent)
setAutostartEnabled()
} catch (ex: Exception) {
Log.d(TAG, "Unable to start for asus");
logFailure(AUTOSTART)
}
}
}
}
A lot of try catches there, as some OEMs have different versions of their OSes which have different package names π€¦. Hence the ladder(s) of try catches in this code.
And finally, the holy trinity of the most godforsaken permissions was made user friendly for Plicly.
All in all most permissions in Android are straightforward and easy to grant, but there are quite a few which have tweaks done to them by OEMs in the name of "security" or "performance". But I guess that's just a trade-off of having the flexibility of open-source to adapt their code for their own needs.
Although painful, it taught me a lot about how structured the entire permissions system is in OSes, specifically Android and a whole lot about how powerful ADB is.
For a new developer, it might be unnecessary or even a hassle to setup. But for power users or people who want to get into the nitty gritties of Android, it is an essential tool to use and learn about.