Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save agnostic-apollo/b8d8daa24cbdd216687a6bef53d417a6 to your computer and use it in GitHub Desktop.

Select an option

Save agnostic-apollo/b8d8daa24cbdd216687a6bef53d417a6 to your computer and use it in GitHub Desktop.

Android Developer Verification Discourse

On August 25, 2025 Google published a blog that starting in 2026, Android will require all apps to be registered by verified developers in order to be installed by users on certified Android devices. This applies to all apps installed from outside the Google PlayStore from either third part app stores like F-Droid, or directly from sites via browsers/file manager apps. Publishing apps on Google PlayStore already has had these same requirements since July 12, 2023. The verification requirements for all apps will engage for Brazil, Indonesia, Singapore and Thailand in September 2026 and engage globally in 2027 and beyond.

There has been lot of noise and backlash regarding the new requirements since then. Malware being deployed both outside and from PlayStore is a big issue and is obviously a cause for concern and a legitimate reason for how developer verification can help reduce it, but not finish it.

I have been thinking over this the past few months and have had a lot of random thoughts regarding this. I finally decided to write a post on this to provide a detailed overview for why and how will these changes will be implemented and the huge amounts issues they will cause. I didn't have time for all this, but there hasn't been positive progress regarding this so I was forced to take time out at expense of other work. I have also been researching into how developer verification will work internally at the Android framework level and have posted the details in the Internal Implementation section.

I am not against the developer verification requirements as malware is obviously out of control, but I am very against Google not providing a non-adb opt-out for users to be able to install apps from unverified developers including open source apps on the devices they own. A huge amount of issues related to package names and signing keys and even legal issues also exist, and that makes the current design not being viable for everyone. In addition to this post I have opened the Android Developer Verification Proposed Changes issue on Google's issuestracker at https://issuetracker.google.com/459832198 with a proposal on how a possible opt out can be implemented.

Edit

Good news! Google has announced in their blog at https://android-developers.googleblog.com/2025/11/android-developer-verification-early.html that:

Based on this feedback and our ongoing conversations with the community, we are building a new advanced flow that allows experienced users to accept the risks of installing software that isn't verified. We are designing this flow specifically to resist coercion, ensuring that users aren't tricked into bypassing these safety checks while under pressure from a scammer. It will also include clear warnings to ensure users fully understand the risks involved, but ultimately, it puts the choice in their hands. We are gathering early feedback on the design of this feature now and will share more details in the coming months.

Contents


 

Overview and Issues

I wrote most of this section before the Epic Games Inc vs Google LLC case settlement was announced, so it is mostly from the earlier perspective, but I have made changes in regards to it too. Additionally, the case settlement requires Google to implement Registered App Stores in future Android versions, but I don't know how it will affect developer verification requirements, but they probably will stay in some form, hence an opt out will still be necessary.

The following subsections quote various texts from the following Google’s developer guides and support questions below, and also from people in the Android developer verification youtube video.

 

Contents

 

How can apps be installed on Android devices?

The app APK files can be installed in the following ways.

Install Without Root/ADB

The are current 2 ways users can install apps without root/adb with normal install mechanism.

Install with INSTALL_PACKAGES

System apps granted the INSTALL_PACKAGES permission with system|signature protection level, that allows them to automatically install applications in background, like the Google PlayStore.

This permission is normally automatically granted to the system apps.

Install with REQUEST_INSTALL_PACKAGES

System or third party apps granted the REQUEST_INSTALL_PACKAGES permission with appop|signature protection level, that allows them to request the installation of the package, which starts Android's own Package Installer app user prompt with Install/Cancel buttons. This is what is normally used by browsers, file managers and third party app stores, like Chrome and F-Droid.

This permission needs to be granted by the users with the Install unknown apps prompt shown during installing if calling app does not have the permission to install apps, or by manually granting it from Android Settings -> Apps (-> Advanced) -> Special app access -> Install unknown app.

On Android >= 12 apps requesting UPDATE_PACKAGES_WITHOUT_USER_ACTION permission can also update their own app or any other app that it previously installed itself in background without a user prompt if certain other conditions are met detailed in PackageInstaller.SessionParams#setRequireUserAction(int).

The REQUEST_INSTALL_PACKAGES permission cannot be granted to apps during phone calls as part of Enhanced Confirmation Mode since Android 15 to prevent scammers from manipulating users into downloading and installing apps from the internet during a call. (1, 2) The Play Protect app scanning toggle is also disabled during phone calls. (1)

 

Install With Root

Apps can be installed on rooted Android devices with su by running the /system/bin/pm install command provided by the Android system as the shell user which belongs to the shell/adb package (com.android.shell) (output="$(su shell -c pm install <args...> 2>&1 </dev/null)"; echo "$output").

 

Install With ADB

Apps can be installed with adb by running the adb install command either from a PC or directly from device, which internally is same as the /system/bin/pm install command provided by the Android system.

The adb command can either be run over a USB connection from a PC after enabling USB debugging in Developer Options, or after setting up Wireless debugging in Developer options.

To setup the Wireless debugging on Android < 11 requires enabling it with USB debugging from a PC on every reboot of the device by running the adb tcpip 5555 command (1), but on Android >= 11, it can be setup directly from the device itself without a PC as a localhost/127.0.0.1 connection (1). Apps like Termux (android-tools package) or Shizuku app can then be used to connect to the Wireless debugging to run commands as adb.

 

 

Epic Games Inc vs Google LLC case settlement affect stores and developer verification?

Proposed Modified Injunction
  1. The Proposed Modified Injunction Would Enhance Competition in App Distribution.

The Proposed Modified Injunction addresses the Court’s conclusions regarding the continuing effects of Google’s past conduct through a new Registered App Store remedy, which replaces the Catalog Access and Third-Party Store Distribution remedies in the Existing Injunction.

The Registered App Store remedy simplifies the process for an Android app store to be installed on user devices and for users to download apps from such stores, while allowing Google to take reasonable steps to protect user security and safety. Those steps are not left open-ended but are instead specified in the parties’ Settlement. The Registered App Store Remedy is also timed to last for roughly six-and-a-half years—over twice as long as the corresponding remedies in the Existing Injunction—which will provide competing app stores even greater certainty and predictability.

The Registered App Stores remedy requires Google to modify the Android operating system to simplify the installation and download flow for all Registered App Stores. When a user downloads a Registered App Store, the user will see a single screen explaining that (1) the store is a registered app store, (2) upon installation the store will be able to install and manage apps on the device, and (3) the entity responsible for the app store will manage downloads and updates for apps distributed by the store. The parties have agreed on neutral language to explain these details to the user. If the user clicks “install,” the store will be installed and the operating system will give it the permissions necessary to install and update apps. With those permissions, the installation flow for apps downloaded and installed through Registered App Stores will be similar to the flow for downloading apps from the Play Store.

To address Google’s view that app and app store downloads could be used by bad actors as a vector for fraud or malware, the parties have also agreed that Google may apply specified neutral safety and security criteria to review stores for certification as Registered App Stores. That process is conceptually similar to a remedy proposed by Epic’s expert at trial. (See Trial Tr. vol. 11, 2156:25-2163:2, Nov. 21, 2023, Dkt. No. 844 (“Android is going to check to see if the app bears that notarization sort of stamp of approval. If so, Android does not show that friction screen . . . .”).) Google will be permitted to charge reasonable fees to cover the operational costs of this review process. And the Technical Committee can provide guidance on any implementation questions relating to this remedy.

The Registered App Stores remedy will promote competition in the Android app distribution market by streamlining the process for downloading qualified Android app stores and downloading apps from those qualified stores, thereby making it easier for Google Play store competitors to offer their product directly to Android users, while also ensuring the safety and security of Android users.

The Existing Injunction took a different approach to this issue, requiring Google to offer, for three years, its catalog of apps to competitor app stores (Catalog Access) and to distribute, for three years, competitor app stores through the Play Store (Third-Party Store Distribution). The Court recognized that “there are potential security and technical risks involved in making third- party apps available, including rival app stores,” and the Court established the Technical Committee to help address them, recognizing that the Court would still need to be the decision maker of last resort. 2024 WL 4438249, at *7. The parties’ remedies filings evidenced significant disagreements over such issues, including (among other things) the mechanism for disseminating and updating the Play Store catalog; the installation flow for apps in Google’s catalog from third- party stores; the method of displaying third-party stores in the Play Store; the installation flow for third-party stores from the Play Store; and the adoption of eligibility criteria, policies, and fees for both Catalog Access and Third-Party Store Distribution. See, e.g., Dkt. Nos. 982-2 (Google’s Proffer), 986-2 (Epic’s Response), 996 (Google’s Supplemental Response). The Proposed Modified Injunction significantly reduces the scope and magnitude of these implementation disputes by adopting an agreed-upon approach that would directly “lower the barriers for rival app stores to get onto users’ phones,” which was the purpose of “enjoining Google from prohibiting the presence of rival app stores in the Google Play Store.” 2024 WL 4438249, at *7. This approach will reduce the need for “continuing supervision” by the Court. NCAA v. Alston, 594 U.S. 69, 102 (2021). To be sure, the parties may come to disagree on certain aspects of the implementation of the Registered App Stores remedy, and the Proposed Modified Injunction preserves the Technical Committee to resolve such disagreements in the first instance. But those disagreements will be narrower and less complex than those that were likely to arise in implementing Catalog Access and Third-Party Store Distribution. The Proposed Modified Injunction, and the Settlement it enables, also address a limitation of the Catalog Access and Third-Party Store Distribution remedies arising from the territorial scope of the Existing Injunction. In Epic’s view, because the Existing Injunction applied only to the United States, even when those remedies were in full bloom, third-party app stores would still face challenges in building the scale necessary to compete vigorously with the Google Play store, with its global footprint. If the Court adopts the Modified Injunction and the Settlement goes into effect, however, the Registered App Store program will result in changes to the Android operating system worldwide, thereby strengthening competition across Android, including in the United States.

Basically as per the new settlement, the previously suggested remedies of Catalog Access (other stores being able to distribute apps that are hosted on PlayStore) and Third-Party Store Distribution (PlayStore hosting Third-Party stores itself) have been replaced with the new Registered App Store remedy.

As per the Registered App Store remedy, future Android versions will allows users to install Registered App Stores with a single screen, and then will allow such stores to manage installs and downloads of other apps hosted on it, and the flow will be similar to downloading apps from the Play Store. Likely this means registered stores would be able to request the INSTALL_PACKAGES permission instead of the REQUEST_INSTALL_PACKAGES permission, and they would be allowed to install (not just update) apps without the package installer app user prompt, like it currently isn't shown for updates if a store has UPDATE_PACKAGES_WITHOUT_USER_ACTION permission on Android >= 12.

The Registered App Store remedy looks to be a great step forward to maintain openness of Android by Google. However, there are currently still some concerns regarding the following part.

To address Google’s view that app and app store downloads could be used by bad actors as a vector for fraud or malware, the parties have also agreed that Google may apply specified neutral safety and security criteria to review stores for certification as Registered App Stores. That process is conceptually similar to a remedy proposed by Epic’s expert at trial. (See Trial Tr. vol. 11, 2156:25-2163:2, Nov. 21, 2023, Dkt. No. 844 (“Android is going to check to see if the app bears that notarization sort of stamp of approval. If so, Android does not show that friction screen . . . .”).) Google will be permitted to charge reasonable fees to cover the operational costs of this review process. And the Technical Committee can provide guidance on any implementation questions relating to this remedy.

  1. The stores that want to get registered would need to be certified, which is expected, but the conditions are currently unknown. If they are too complex or they require too many resources/money/monitoring, then smaller stores (maybe even F-Droid) may not be able to fulfill the requirements or may not want that responsibility.
  2. Who will do the certifications, is it Google, someone in the US or device vendors? If that is Google or someone in the US, then the Will Google block apps of developers from specific countries?, Will Google block apps of developers that are engaged in court disputes with Google and other US companies, or competitors of US companies? and Will this be used for policing Android developers worldwide? sections still apply to developers of the app stores.
  3. Since changes are to be added in future Android versions and it will not be possible to grant INSTALL_PACKAGES or change flow of installation on older Android versions, how will this affect the certification process. Will changes in Google Play Protect being added for developer verification backward compatibility also be used for approving installation of registered stores.
  4. How does the settlement affect the Android Developer Verification requirements? Will Registered App Stores be responsible for apps on their own stores, or will apps both on Registered App Stores and distributed outside them still need to get verified by Google?

Note that a lot of open source apps are distributed from GitHub, GitLab and other source code hosting sites, and those are not "App Stores", and apps distributed on them will still need to be installed via the browser/file manager apps.

So there should still be some way (like as proposed) for non-registered stores to be installed, and apps on them and apps outside any stores to be installed with normal install mechanism like it currently is on Android <= 16, without developer verification and without root/adb.

 

 

How will limited distribution work for hobbyist/students?

I'm just a hobbyist/student. What is "Limited Distribution"? We understand that not everyone is a professional developer. We are introducing a free developer account type that will allow teachers, students, and hobbyists to distribute apps to a limited number of devices without needing to provide a government ID. Last updated: Sept 11, 2025

NAHEED VORA: So I think the only thing is you need to bring an email, and that's about it. And it's that simple. It's as simple as that. TOR NORBYE: Isn't that a major loophole then for a bad actor to say, yeah, yeah, yeah, I'm only going to share with my friends. But then actually, do we have a way to count? How do we make sure that it isn't going to be mass distributed? NAHEED VORA: Yeah, so I think the way we have been thinking about it is that number of devices that you can install on would be limited. TOR NORBYE: So when I install, that gets counted? PATRICK BAUMANN: The way that we've been designing this piece of it is that you as a user, if you would like to get software from someone who's in this program, you give them an identifier from your device. There's a unique identifier that we're generating specifically for this purpose. There's kind of a back and forth established relationship with the developer. TOR NORBYE: OK. So the actual publishing part is different? NAHEED VORA: That's right. It's just the two way handshake that, hey, that user understands. And you as a developer can send an invite. They can throw back a token that you put in the Console. And then from there, you can go and send them apps to install on their device.

 

 

How will ADB work?

Will Android Debug Bridge (ADB) install work without registration? As a developer, you are free to install apps without verification with ADB. This is designed to support developers' need to develop, test apps that are not intended or not yet ready to distribute to the wider consumer population.

For people saying not to worry, adb will still work, in Google's own words, adb is considered to be used for "test apps that are not intended or not yet ready to distribute to the wider consumer population", that means it is not meant to be used as a normal install mechanism for "release" apps that are distributed by stores like F-Droid or GitHub for consumers. It also means limits could be put in place in future on number of installs, just like they are for hobbyist/student.

This view is also confirmed from the Android developer verification video:

RAZ LEV: If you're going to install it on your own device using ADB... then you're not going to need to verify... If you want other people to use it, then you'll need to verify because now other entities are involved. We want to keep those people safe.

 

RAZ LEV: But there will be backwards compatibility leveraging Google Play Protect. And that's going to have slightly different behaviors in some cases, just because of the difference between a native verifier in the operating system versus leveraging an existing APK. But the goal is to give global coverage to all Android devices.

Verification is going to be backported to all android versions, and adb wireless only works for Android 11+, and requires a usb connection from PC on older android versions on every reboot, and often has device driver issues. While Termux users using developer options are partially expected to be developer minded to be able to run shell commands, normal app users who install normal apps from stores are not. Additionally, people still on older Android versions are more likely to not own secondary devices like a PC to be able to run adb commands anyways.

And if Google still suggests using adb for sideloading, will they then stop approval of apps on PlayStore (like bank/food apps) that refuse to work if Developer Options is enabled?

 

 

Are sideloading and other stores going away?

MATTHEW FORSYTHE: Well, I think sideloading, I think, is one that comes up quite a bit. And I do think it's one that maybe is not as understood as well. And so I think, as you mentioned before, sideloading isn't going anywhere. It's very much core and central to Android. I think just to be really clear and put a fine point on it, developers will be able to distribute their apps through sideloading or other stores as long as they're verified.

If all other stores need to get approval from one single store/company on most Android devices (apparently >90% of all Android devices outside China), then there is basically only one store. Google shouldn't simultaneously both be the owner of the primary store and also the only verifier of all other stores on almost all devices.

 

 

Why is there a $25 fee?

Why is there a $25 fee for the ADC? How can I pay? The $25 fee for the Full Distribution account in the ADC helps cover administrative costs and investment in protecting the ecosystem, similar to Play's $25 registration fee. We are actively working to support multiple forms of payment to accommodate developers globally and will have more details when the console launches. We are waiving the fee for developers who qualify for a Limited Distribution account. Last updated: Sept 3, 2025

Google won't be able to receive money from restricted countries. And developers in approved countries do not have always have payment methods available to pay for services themselves, even if Google supports them. This is a common problem for developers who want to pay for a VPS, etc, who are in China/Russia, etc, including our own Termux team.

All developers would also need to be above 18 years of age since money and contracts are involved.

More reasons below why there is a $25 fee:

TOR NORBYE: Speaking of the Play Store, so you mentioned earlier that we had done this already and we had good results. Can you share more about what those good results were? What data did we see that concluded that, well, this is actually the way to go? RAZ LEV: I don't remember the numbers off the top of my head, but we've seen double digit reduction in bad actor activity on the Play Store. It just became too difficult for them. The way that a lot of the bad actors operate in these type of marketplaces is they create a lot of accounts. And by a lot, I mean, huge numbers, thousands, tens of thousands, hundreds of thousands. So when you think about a legitimate developer that says, hey, $25, that's a big fee. And I need to give my ID and that's difficult. Now imagine somebody having to do that tens of thousands or hundreds of thousands of times. It becomes really, really difficult. And even if you do it, it's very difficult to do it in a way that is unnoticeable. So you do it, and all these issues start showing up. And that's really turning our attention to these accounts. And then we can take care of them. And those bad actors don't monetize. They don't earn money. It's more difficult for them to sustain these operations. That's been going on on Play for a while. And we're very happy with how it's working.

 

 

How will registering existing and new package names work?

TOR NORBYE: Is this going to be like handles on new social media sites, where there's a land grab? If I'm early, can I grab com.facebook.messenger? RAZ LEV: We're not going to let you do that. So we're going to put in place governance for existing APKs. For future APK names, you will be able to grab whatever names you want. TOR NORBYE: So there's no namespacing with-- if someone else has a package name, can I grab something similar to it? Do we do any kind of limitation there? RAZ LEV: No, we don't. We've actually given it a lot of thought. And the package names are not really visible to users. So grabbing a package name doesn't give you a huge advantage. You can anyway pick-- TOR NORBYE: I see it in the Play Store URL. RAZ LEV: You're right. You see it there. And some stores show it in a pop up when you download an app from them. But it's pretty hard to find it and you need to be very mindful to look for it. The thing that really matters is the app icon and the app name that shows up on the device. And we're not going to start managing those. The situation stays as it is in Android.

Basically open source "forks" of apps with same package name won't be able to register themselves, unless the original owner who already had the app on PlayStore registers for them. This applies for Termux too for F-Droid/GitHub releases.

 

 

How will signing challenge work?

Android Developer Console will provide you with a snippet which you need to copy and add to an APK's asset folder. You'll then need to sign the APK, and upload it in Android Developer Console. We'll provide a sample project so you can see the required file structure. This APK is used only for the purpose of verifying ownership, and you won't need to upload the actual APK that you distribute.

I assume the private key needs to sign an APK instead of a text file so that apksigner can be used and normal build process can be used. Will there be an expiry on the token in case it will take time for the developer to sign it, like some formal process of signing in an air-gaped machine, etc, like F-Droid which normally takes 3 days to manually sign.

 

 

How will verification work at install time?

PATRICK BAUMANN: In the operating system, we built a hook into the install flow. So any kind of install has to go through this verification. At the time of install, we reach out to a trusted entity on the device, the developer verifier. It's a preloaded app that has-- it's exclusive, so there's only one on the device. So there's a single trusted entity that we ask whether or not there's a verified developer behind a package that we're installing. We'll pass the package name and the hash of the signing key as the identifier for it. And then we're told by the verifier, both whether or not it was verified if it runs into any issues while verifying, and then what policy we should enforce. TOR NORBYE: OK. So my phone has to be connected in order to install an APK? PATRICK BAUMANN: In the worst of cases, yes. TOR NORBYE: OK, what is the best case? PATRICK BAUMANN: So in the best case, you hit a cache. So this trusted entity, the developer verifier, can build all sorts of things on top of that basic back and forth. They're building a cache of-- to be determined what will be contained in it, but presumably, popular apps.

There is more information regarding this in the Internal Implementation section below. The verification check is made on all installs, even updates in case developer was previously verified, but is not verified anymore.

There's also a pre-auth-- we call it a pre-auth token. It's basically a cryptographically verifiable blob that is associated with the package that's being installed that the installer can pass in alongside the app and verify the developer without having to hit backend. So if you're installing something from a store, as long as they can get the bytes for the APK, they can presumably also get the bytes for the pre-auth token and avoid any additional network back and forth.

So it should be possible to install "approved" APKs without an active internet connection. Possibly backup apps can also backup the pre-auth token and use it during restore. All old backups created without this pre-auth token will not be restorable, unless adb or root is used, so users will have to create new backups and backup apps will need to add support for using pre-auth token.

 

 

How will enterprise devices work?

TOR NORBYE: Any other complaints, or misunderstandings, or legitimate complaints maybe that you've heard? NAHEED VORA: There are some nuances about certain scenarios. Maybe I don't know if you want to talk about enterprises and their apps, perhaps. RAZ LEV: Yeah, we can mention that quickly. So Android is very big and used by a lot of people. And over the years, many, many, many use cases have been built over it. And such a big fundamental change is making some of these very niche solutions either not work or require some adaptations. So for example, entities that distribute apps on devices that are not connected to the web in any way are going to need to figure out how to either leverage the tokens that Patrick mentioned or create a connection once in a while to get the permissions. We've made exceptions for enterprise management solutions. In an enterprise setup, there is an IT admin. And it's their responsibility to vet the apps that their users are installing. So if someone's using an enterprise management solution, they would be able to install apps that are not verified.

 

 

How will verification affect F-Droid and other stores?

F-Droid and other stores would have to register the signing keys of their own store APKs with Google, and if Google doesn't approve them or revokes them in future, users won't be able to even install the store itself, let alone the apps on the store (unless browser is used). So either the store app will need to be preinstalled on the device by the vendor or users would need to use adb.

The Epic Games Inc vs Google LLC case settlement requires Google to add Registered App Stores support in future Android versions, but not all stores may get registered.

Note that Google PlayStore currently also does not allow apps of other stores on it as it violates its policies, that's why F-Droid store is not available on Google PlayStore either.

4.5 You may not use Google Play to distribute or make available any Product that has a purpose that facilitates the distribution of software applications and games for use on Android devices outside of Google Play.

 

 

How will verification affect apps on F-Droid and other stores?

F-Droid has published their responses at following links. There are lot of other stores too, like Samsung, Epic, and lot of chineses ones belonging to xiaomi, huawei, tencent, etc.

F-Droid by default signs APKs themselves in an air-gaped machine and does not give access for private keys to developers, but it does support developers to sign with their own keys since 2023 if they can get reproducible builds working, or host their own repo. From my older quick check, of the 8000 apps on F-Droid store main repo, only ~700 were being singed by developers themselves.

What this means is that developers who have been releasing apps on F-Droid will not be able to complete the dummy APK signing challenge required for developer verification to prove that they own the private key.

F-Droid cannot register themselves for all package names themselves because:

  1. They do not own the package name/reverse domain name, that belongs to the developers.
  2. If a developer is also releasing the app on PlayStore, they would have already registered the package name in their own account.
  3. If F-Droid registers all package names on behalf of all developers, and if one of the package gets flagged to contain malware, all package names would get blocked and won't be installable anymore as F-Droid's identity would be flagged.

One way to solve this issue is if F-Droid were to provide some portal for developers to submit dummy APKs with tokens that they then sign with the private key on behalf of the user. Of course that would requires some kind of authentication mechanism so that only developer team themselves can submit the request instead of some random person trying to take over the registration of the package in PlayStore. F-Droid itself does not have user accounts currently where developers registers and requests to add packages are just sent on gitlab via issues/pulls. Maybe domain emails or gitlab accounts itself could be used and since hopefully Google generates a unique token for every user trying to register a package, the dummy APK submitted to F-Droid by owner developer cannot be used by someone else even if posted publicly online. This could of course lead to problems where different members of the same app project try to take over PlayStore registration by asking F-Droid first if app is not already on PlayStore, so F-Droid will need to set up some policy.

F-Droid keeping private keys themselves and not giving access to users is not a F-Droid specific issue, Google PlayStore does the exact same thing with "Google-generated app signing key", which is used for 90% of all new apps, which then begs a question. If Android supports custom VerificationServiceProvider (check Internal Implementation section below), then how will developers on PlayStore register their package names on other service providers if they don't have access to the private key. So Google will also need to implement a similar portal to sign dummy APKs with the private keys they themselves are keeping. So is Google going to implement such a portal? If not, then PlayStore apps will not be installable on such devices with non-google verification service providers and vendors would be forced to either use Google verification service provider, or at least still have it installed on the device to call its service from their own service provider as a fallback assuming that's supported by calling DeveloperVerifierService.onVerificationRequired(DeveloperVerificationSession) without going through PackageManagerService.

 

 

How will this affect Termux app?

It will be DEAD! Well, might be.

Termux currently distributes com.termux package on 2 sources officially from termux GitHub org (https://github.com/termux/termux-app) and both have a different signing key:

  1. F-Droid: Termux team does not have access to the private signing key and only F-Droid does.
  2. GitHub: These are debug builds and used by users who want to use latest features on master branch before releases are made, or want to send pull requests, or want to use adb shell run-as com.termux to run Termux commands from PC. The private key for this is public as developers and contributors need to be able to develop locally and test pull requests without having to uninstall/reinstall a differently signed APK. (https://github.com/termux/termux-app/blob/master/app/testkey_untrusted.jks) These builds are also heavily used by our users and there are currently 3 million+ downloads for our last major 0.118.0 release.

Additionally an experimental fork of the com.termux package is also distributed by the original creator of Termux app on PlayStore from his own termux-play-store GitHub org (https://github.com/termux-play-store/termux-apps) and Termux team in termux GitHub org neither has access to termux-play-store org or the Termux app PlayStore account. Only the original creator has access and he hasn't transferred the access to termux org yet, and it does not seem like that will change. For some more info on this issue, check termux/termux-app#4000. The PlayStore release also has its own signing key.

So how will this situation affect Termux:

  1. F-Droid will need to provide a portal to sign the dummy APK, if it does not then Termux will not be installable anymore from F-Droid. Even if F-Droid does provide a signed dummy APK, the termux org will not be able to register it, as we do not have access to the PlayStore account, and it will require the original owner to submit the signed dummy APK on our behalf.
  2. GitHub releases private signing key being public can be an issue for Google, I do not see anything mentioned regarding it in the docs. Other open source projects would also be using public keys. If Google does not allow private keys being public, which is likely as private keys are meant to be tied to individual identities, then Termux will not be installable from GitHub either. If Google does allow it, it will also require the original owner to submit the signed dummy APK on our behalf.

However, the original creator registering signing keys on behalf of termux org is not viable as that will give them exclusive control over F-Droid and GitHub too, in addition to PlayStore. We very likely will not be able to register our own signing keys of the com.termux package ourselves from a different account as the original Termux app has existed on PlayStore since 2015 published from the original creators account. So unless original creator transfers the PlayStore account and termux.com domain to termux org, the termux org team will just fork the Termux project and start anew, which will be months of work for us, not to mention millions of users will need to shift.

Which then leaves two possible outcomes:

  1. The PlayStore account gets transferred to termux org and then we are able to register existing F-Droid and GitHub signing keys, depending on if F-Droid provides a portal and Google is okay with the publicly available private key. If either key does not get registered, users will need to backup their termux app data with tar/termux-backup and install an APK whose signing key was approved, and restore data on new install. If neither key gets approved, or even regardless, we already had created a new signing key that was to be used for releasing APKs from our site/custom F-Droid repo, etc, so we can register that, its signing key will not be publicly available and users can shift to that build.
  2. The PlayStore account does not get transferred to termux org and we fork, in which case users will run into issues. The termux native (apt) packages are compiled specifically for the /data/data/com.termux path that Android by default assigns to the com.termux package for its app data directory, and the packages will not work if restored in an app with a different app package name as app data directory path will be different. However, I am in the process of implementing dynamic variables, and if it works, then it should be possible to restore a com.termux native packages backup in our forked app, but there could still be issues. I have already implemented the new shell environment from the app side required for it as part of termux-app rewrite, but changes to libtermux-exec.so $LD_PRELOAD library still needs to be implemented. If it does not work, then our millions of users will have to install all packages from scratch and setup their environment again, which would be insane amount of work overall for all our users, not to mention us having to deal with the insane amount of support issues created by users for it. Note that while Dynamic Variables design can be used to run com.termux packages in another app package, there will be a runtime cost due to $LD_PRELOAD hooks, I haven't tested how much.

By not installable, I mean by normal install mechanism, adb would still work, unless max install limits are added in future.

Note: The private signing key being publicly available for GitHub releases is not really a security risk for Termux since if someone has access to the device, they can just open the terminal to access all of the Termux data without having to install a malicious APK like the case of other apps, or use the Termux SAF provider. A malicious APK from random sources could also be installed over current installation, but a user can just download malicious code directly inside Termux itself instead of installing an APK. Our users are already warned in our install docs to not install Termux APKs from random sources.

Termux's situation is unique partly, because of pre-existing PlayStore issues and Google is not to blame for it all, but this verification requirements will just exacerbate the situation, and the F-Droid/GitHub issues still apply to most other open source apps too. I still wanted to mention our situation to let Google be aware of it, and also allow other open source projects to prepare themselves who may have similar issues, and also for our users who would/have been curious about future of Termux too.

 

 

How will this affect Open Source apps?

Open source apps primarily have 2 issues.

Shared Package Names

Open source apps share package names for forks. Forks might either be done for two reasons, for sending pull requests to upstream, or to maintain an independent project fork of upstream.

For sending pull requests, verification requirements shouldn't affect anything as adb can be used, unless max install limits are added in future. But it can affect independent forks, as normal consumer user will need to install the apps with normal install mechanism and not only developers with adb. If forked app developers won't be able to register their signing keys, then normal users won't be to install the app.

Changing the package name is not always viable as package name is often hardcoded through out the source code and would require significant effort trying to patch those while keeping in sync with upstream. Some projects may design their projects better and use static constants for package name references in the source code to support forks, like Termux, but not all resource files like xml files allow build time placeholder changes, like shortcut files, etc. Those require manual replacement or unstable gradle hacks. Additionally, there could be other issues for changing package names that may be project specific, like project dependencies in other repos or native code could be using package name too. Like for example for Termux, all our 3000+ linux packages are compiled for com.termux package name and they can only be run from under Termux app data directory /data/data/com.termux/files/usr that Android by default assigns the com.termux package if installed in primary user 0, and if a fork changes the package name, they would have to recompile all 3000+ packages and host their own apt repos for the packages and won't be able to use our 70+ mirrors. It is an insane amount of work to do that.

Shared Signing Keys

Open Source package names may have the private signing key publicly available for master branch builds and/or debug/beta releases, as developers and contributors need to be able to develop locally and test pull requests without having to uninstall/reinstall a differently signed APK.

Additionally, often times errors are reported for current configuration of the user, and asking user to uninstall and then reinstall to test a fix would not work as they would have lost their current configuration if its not reproducible.

Since these non-stable builds can be used by normal users, they won't be able to install with normal install mechanism if private signing keys being public and not allowed.

 

 

How will this affect APK Native Library?

A new APK Library File (APKLF) execution/packaging design will be implemented as part of NLnet mobifree grant so that Termux can comply with security restrictions added in Android 10 prevents apps from executing downloaded packages/binaries if an app uses targetSdkVersion >= 29 (Android 10). As a workaround Termux is currently using targetSdkVersion = 28 (Android 9) to run in backward compatibility mode. APKLF design is very critical for long term functioning and stability of the Termux app, as the exemption allowed by Android for apps like Termux to execute downloaded files by using targetSdkVersion = 28 (Android 9) may end in some future Android version, which will break Termux completely.

Check the App Data File Execute Restrictions, including the untrusted_app* SeLinux Policy section for more info on the Android security restrictions. Check the termux/termux-app#1072: No more exec from data folder on targetAPI >= Android Q and termux/termux-app#2155: Revisit the Android W^X problem issues for discussions regarding the issues and possible solutions. To understand how Termux executes files, check the Termux Execution Environment docs.

Check the APK Native Library docs for more info on the APKLF design, and the Issues sections for details on all its issues that need to be solved. A proper design doc and info on additional issues will be published in near future.

Android allows both dlopen() and exec() of files that were added as native libraries to the APK during build time and then extracted to APK native library directory (ApplicationInfo.nativeLibraryDir) under the /data/app/*<package_name>/lib/<arch> directory during install time. The extraction can be enabled with the extractNativeLibs/useLegacyPackaging options. (1)

The native library files of other apps can also be executed even if their APKs have been signed with a different signing key or their sharedUserId is different or isn't set, so if you cannot add all the executable files and dependencies to a single app, additional plugin apps can be created and installed.

With the APKLF design, for each Termux deb/apt package, an APK file will be created in which binaries will be hosted, while rest of the package files will be in the data.tar file of the deb file. With current tentative design, the APK file will be hosted inside the deb file if deb is to be hosted on our apt repo, or it will be a separate file if APK needs to be published on the store. During package installation inside Termux, the package's respective APK will be installed as well and symlinks created inside $TERMUX__PREFIX to its respective files in APK native library directory /data/app/*<package_name>/lib/<arch>.

During my research for APKLF, the issue I found with publishing APKs on the store instead of hosting them inside deb files is that if thousands of packages need to be distributed as APKs on Google PlayStore, then it will be a fast-paced experience! Even though there does not seem to be a limit on the total apps that can be published on the Google PlayStore under a single account, only 15 new apps can be published per day apparently. (1, 2) Termux repositories currently have 2692 packages with an additional 397 subpackages, totaling 3089 total packages (1, 2), so to upload them all as separate APKs for the first time on PlayStore would only take 3089/15 = 206 days (6.8 months), assuming you don't miss a day and even work on weekends, without weekends it would be only take 9.4 months. Each package app would also have to setup in the PlayStore account separately and additional review time would be required by Google to review each app, which could be "seven days or longer in exceptional cases". These packages will not declare any permissions, not even the INTERNET permission, and won't require any activities/services/providers/receivers, so review time could be less. This time does not include the time that would be required to fulfill the testing requirements for new apps where you are supposed to run a closed test for your app with a minimum of 12 testers who have been opted-in for at least the last 14 days continuously.

After that, whenever an update to an app needs to be published, it will requires additional review time, which again could be "up to 7 days or longer in exceptional cases". (1) Sometimes Termux package updates to apt repositories are broken, or they break other dependent packages due to dynamic library linking errors, in which case emergency updates need to be done for all the affected packages within a few minutes or hours to minimize affected users and any delays cause more and more issues being reported by users in multiple community channels and they all need to be triaged. A delay of "seven days or longer" would cause massive problems for users who will have a broken environment until fixed updates are available to them and us for having to triage the tonne of issues reported.

This wouldn't be a problem if APKs are hosted in deb files on apt repositories outside the Google PlayStore from which packages can be downloaded, but that would not be compliant with Google PlayStore Policies. So unless Google Play Console grants exceptions to upload more than 15 new packages per day, without testing requirements, and without a 7 days or longer review process for updates, APKLF solution will not viable for PlayStore. I personally won't be spending thousands of hours dealing with that mess every day for months and years.

An app distributed via Google Play may not modify, replace, or update itself using any method other than Google Play's update mechanism. Likewise, an app may not download executable code (such as dex, JAR, .so files) from a source other than Google Play. This restriction does not apply to code that runs in a virtual machine or an interpreter where either provides indirect access to Android APIs (such as JavaScript in a webview or browser). Apps or third-party code, like SDKs, with interpreted languages (JavaScript, Python, Lua, etc.) loaded at run time (for example, not packaged with the app) must not allow potential violations of Google Play policies.

Additionally, with these new verification requirements, if there is going to be the same/similar limit that only 15 packages and their signing keys can be registered per day, then we are in the same mess again, except additional time for testing and review on updates won't be required. So it there a limit of max verifications per day?

 

 

What if your national identity card gets stolen?

Android’s scam defenses protect users around the world from over 10 billion suspected malicious calls and messages every month. In addition, Google continuously performs safety checks to maintain the integrity of the RCS service. In the past month alone, this ongoing process blocked over 100 million suspicious numbers from using RCS, stopping potential scams before they could even be sent.

These malicious calls and messages that users receive around the world are (would be?) primarily initiated from stolen phone numbers. These scammers wouldn't be using their own phone numbers, especially any scammer with more than one brain cell, let alone sophisticated ones. The same thing will happen for the individual account verification process with government-issued identity document and one-time password sent to your contact phone number. Stolen IDs and phone numbers will get used for verification, as these verification processes only check possession of IDs, not that you are the true owner. It would be informative to know the percentage of stolen IDs that have been used to verify standard Play console developer accounts since July 12, 2023, since millions upon millions of malicious app installs are still being deployed from PlayStore, all those are surely not using real identities, so how useful has verification actually turned out to be. There is a double digit reduction reference in the Android developer verification youtube video, but that could be anywhere between 10% to 99%, and does not mention use of stolen IDs.

Here's a recent example of Linkedin account being used in a scam that was verified by Persona (Microsoft's partner service), likely with a stolen ID.

WhatsApp has the similar issue with verifying if a phone number actually belong to you or was stolen, and they use a 7 days waiting time to partially mitigate it if attacker enables 2FA after taking over your account.

Additionally, what happens if your stolen ID was actually used to verify and deploy malware. If your identity is going to be blocked by Google, can you never distribute apps again yourself? And what would be the way to convince Google that it wasn't actually you before, but is now, as at the time of verification only possession was verified and not ownership. Would a police report be enough and how long will the process take? Google accounts being blocked normally are not recoverable, at least easily. To prevent such situations, should every developer who may wish to ever distribute apps on Android as part of the rite of passage upon reaching the age of 18 preemptively verify themselves with Google so that in case their ID ever does get stolen, the thief cannot register themselves using it?

TOR NORBYE: Was there some sort of in-person video call or something, or is this going to be just upload pictures, or is there some sort of face to face, prove that you're a human kind of meeting to do this? RAZ LEV: No, there's no face to face, prove you're a human meeting. There's always exception cases where contact is required. But those are very, very rare.

Google would have to always do live facial scans at time of verification and compare them against the government ID photo to more reliably know if its actually you registering and not someone else who has stolen the ID. That's the process that online banking platforms normally use. Is Google planning to do that too in future if required?

 

 

Will Google block apps of developers distributing apps even outside PlayStore?

Without limiting any of our other rights, Google may suspend or terminate your access to the services or delete your Google Account if any of these things happen: we’re required to do so to comply with a legal requirement or a court order

Yes, to comply with legal requests from US government, and other (totalitarian) governments, they can suspend the Google account of developers, which would be required for developer verification to work for approved signing keys.

 

 

Will Google give identity info of developers distributing apps even outside PlayStore?

We will share personal information outside of Google if we have a good-faith belief that disclosure of the information is reasonably necessary to: Respond to any applicable law, regulation,legal process, or enforceable governmental request

Yes, to comply with legal requests from US government, and other (totalitarian) governments, in addition to just suspending the Google account of developers, they may also give the real identity of developers to governments, putting them in harms way. This includes developers of censorship resistant apps (like VPN, encrypted chats), or apps in which developers or its users "may" violate copyright (like emulator gaming).

The identity info of developers won't be shown publicly though, like it is for developers distributing on PlayStore.

More info at:

 

 

Will Google block apps of developers from specific countries?

Yes, to comply with legal requests from US government. We all know both sides of the US government has been having trade wars with China for past many years, and even more so this year with even more countries. Additionally many countries are under sanctions. These trade wars and sanctions have resulted in policies being used politically that prohibited US companies and even their partners outside US to work with companies from restricted countries. With this new verification implementation, Google can be "used" to legally prohibit verifying developers and companies from countries the US is having trade wars with to get "better" trade deals, like say the EU or China. Taking money ($25 fee) to verify from these countries while restrictions are in place would additionally complicate things too.

Google and others may say this will not happen, but it already does happen. Only approved countries can create accounts on Google Play, and some countries are not allowed, like say Iran. So developers from restricted countries would not be able to verify themselves and distribute apps for the Android platform themselves. If they want to distribute, they will need help from people in specific countries that do not have restrictions on working with the country of the developer.

Not allowing developers from specific countries to distribute apps even outside PlayStore at a global scale would be obvious discrimination against certain classes of people who had the random luck of being born in a restricted country, over which they themselves had no control over. This would not just affect businesses, but individual developers. At least before these verification requirements, developers have been able to distribute apps outside the PlayStore (or without Google's approval), and users around the world have been able to use apps from developers of these countries.

There are some other restrictions for countries where you can distribute apps to.

US government is obviously free to set its own policies, but a globally used Operating System like Android should allow users outside the US to have their own policies and not be forced to live under US policies.

 

 

Will Google block apps of developers that are engaged in court disputes with Google and other US companies, or competitors of US companies?

Whether Google would do it or not, they definitely will have the ability to do so after this to prevent users from installing such apps from even outside PlayStore. Apple had that same ability to block such apps system wide, and they blocked Epic Games store. Google previously blocked Play accounts of Epic Games store, but it has still been installable from outside PlayStore. While Google has settled its case with Epic for foreseeable future, other non-US companies and competitors can still be affected, including when getting certified as Registered App Stores.

 

 

Will Google block or not verify apps that violate its content policy?

Currently, this is meant for malware, but will it be expanded to content policy as well, is there any reason to trust why it wouldn't be for some minimal version of content policy in future? And if yes, will every developer distributing apps only outside PlayStore go through a similar approval process like that for apps distributed on PlayStore, which very often is horrendous?

 

 

Will this be used for policing Android developers worldwide?

Both extremists on right and left have been engaged in censorship and cancellation of people, and social media accounts of such people often get blocked, depending on who owns the platform and current government who rules, etc. These blocks may often be justified, but not always. Current US government has also added policies in their visa policies where they require applicants to make all their social media accounts public and submit links to them so that they can be vetted to see if the applicants have said something online against specific "entities". If now Android developers worldwide need to get verified from a single authority in the US, then they best not say anything controversial online against these specific "entities" anymore, otherwise their ability to distribute apps even outside PlayStore can be blocked, disrupting their whole career as an Android developer.

 

 

What about anonymity?

RAZ LEV: I think that the toughest sort of feedback we've heard is, hey, anonymity is going away. And I understand that. There is something very fundamental about being able to distribute apps anonymously. The tension is obvious. Anonymity is good for people who need it for good purposes. But it's also very, very convenient for bad actors. And we think we reached a point where that trade-off is causing much more harm than good, which is why we need to change the balance a little bit.

Lot of people are okay with anonymity, let them have a way to exempt these restrictions. FOSS still hugely works on anonymity. Real identity is more relevant for closed source apps on PlayStore, and does it really matter if apps on PlayStore are constantly tracking and uploading private info of users. In open source, primarily the code matters, not the identity. In the wise word of Linus Torvalds "I think people can generally trust me, but they can trust me exactly because they know they don't have to".

 

 

Google contacting other stores

TOR NORBYE: Yeah, I mean, I guess with the Play Store, we can access. We can notify developers. But what about other stores? Have we talked to them? Do we have a sort of sense of how they feel about this, and whether they can help bring the message out to their registered developers? RAZ LEV: We're engaging with stores. There's a lot of actors in this ecosystem. We're engaging with as many as we can. And we're counting on them to spread the word. We're counting on media and press to spread the word. One of the reasons we're here today is to spread the word and make sure that people are aware.

I wonder if F-Droid was directly contacted...


 

Internal Implementation

Android 16 QPR2 Beta 3 (build_id: BP41.250916.009.A1, security_path: 2025-10-05) has the following implementation for Developer Verification internally, only relevant and important parts are posted.

Internally there are two app packages used for developer verification management.

Developer Verification Service Provider Package

The provider package verifies if the installation package is verified or not. The provider package requires android.permission.DEVELOPER_VERIFICATION_AGENT, which has signature|privileged protection, and so it must be a system privileged app that comes pre-installed on the device.

This provider package hosts the DeveloperVerifierService android Service that implements the IDeveloperVerifierService binder interface which is called by PackageInstallerSession.handleInstall() -> PackageInstallerSession.performDeveloperVerification() -> PackageInstallerSession.startDeveloperVerificationSession() -> DeveloperVerifierController.startVerificationSession() -> IDeveloperVerifierService.onVerificationRequired().

The IPackageInstaller.getDeveloperVerificationServiceProvider() method can be called to get the developer verification service provider package and seems to be shared for all users as there is no int userId argument, even though pm install get-developer-verification-service-provider has a --user flag in its help output (bug?), but its ignored in code. No privileged permission is required to get it, other than package visibility rules being applied.

Developer Verification Service Policy Delegate Package

The policy delegate package to set the policy for the verification. The policy delegate package must be a system privileged app that comes pre-installed on the device.

To get policy with IPackageInstaller.getDeveloperVerificationPolicy(int userId) method requires being the policy delegate package, or if caller has android.permission.DEVELOPER_VERIFICATION_AGENT which both the verification service provider package and com.android.shell package used by adb shell are granted. The valid verification policy values are: - PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE = 0: Do not block installs, regardless of verification result. - PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_OPEN = 1: Only block installs when verification fails. - PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_WARN = 2: The same as fail open. - PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_CLOSED = 3: Block installs when verification fails or cannot perform.

Both the developer verification service provider and policy delegate package can set the policy with IPackageInstaller.setDeveloperVerificationPolicy(int policy, int userId) method. Note that as the com.android.shell package used by adb shell only has the DEVELOPER_VERIFICATION_AGENT permission granted, it can only get the policy, and not set it as it's not the developer verification service provider or policy delegate package.

The IPackageInstaller.getDeveloperVerificationPolicyDelegatePackage(int userId) method can be called to get the developer verification policy delegate package and can be user specific. Only the developer verification service provider package can get the policy delegate package name and having android.permission.DEVELOPER_VERIFICATION_AGENT permission is not enough, hence no pm command for it.

 

Basically, both the developer verification service provider and policy delegate packages need to be system privileged apps that comes pre-installed on the device and are defined in the core/res/res/values/config.xml file provided by /system/framework/framework-res.apk file. The config_developerVerificationServiceProviderPackageName and config_developerVerificationPolicyDelegatePackageName resource values are loaded by the PackageManagerService at boot, and there is no config option to change them currently. In BP41.250916.009.A1, neither resource values are currently set and so verification isn't engaged.

The IPackageInstaller interface provides some basic methods related to verification service provider and policy delegate. The PackageInstallerService internally implements the IPackageInstaller interface and its methods. The methods can be called either by the public PackageInstaller class (PackageManager.getPackageInstaller()) or by PackageManagerShellCommand (adb shell pm). However, PackageInstaller class does not provide methods to add/clear experiments, those are only provided by PackageManagerShellCommand. Caller permissions are of course enforced for each method.

The policy values are persisted in the /data/system/install_sessions.xml file.

$ adb shell "su root abx2xml /data/system/install_sessions.xml -"
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<sessions />
<developerVerificationPolicyPerUser userId="0" developerVerificationPolicy="0" />

See also EXTRA_DEVELOPER_VERIFICATION_FAILURE_REASON for how verification failures are to be handled for verification connection infeasible/failure/timeout, verification incomplete or developer verification failure. This is handled by DeveloperVerifierCallback and PackageInstallerSession.shouldSendUserActionForVerification(). Currently if policy is not set to PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE, then:

  • For installer app that uses targetSdkVersion > 36:
    • If verification request was completed and developer was not verified, then installation will directly fail.
    • If verification request failed to complete (like due to network issues), only then user may be asked to bypass verification.
  • For installer app that uses targetSdkVersion <= 36 (Android 16 and below):
    • If installer app does not have INSTALL_PACKAGES permission, and either verification request was completed and developer was not verified, or verification failed to complete (like due to network issues), then user may be asked to bypass verification.
    • If installer app has INSTALL_PACKAGES permission, then installation will directly fail.

I assume PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_WARN, while currently being same as PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_OPEN, exists so that in future it might be possible to warn user instead of directly failing if verification failed to complete or developer was not verified.

See also GooglePackageInstaller app (com.google.android.packageinstaller) section on how it handles bypassing verification.

The PackageInstallerSession.shouldUseVerificationService() method also checks if install session was started with adb (PackageManager.INSTALL_FROM_ADB) to bypass the verification.

Note that for rooted users it should be possible to create a Xposed/LSPosed module to disable the developer verification hook used at install time.

 

Contents

 

pm

The /system/bin/pm command adds some commands and options related to developer verification.

$ adb shell pm -h
Package manager (package) commands:
    install [-rtfdg] [-i PACKAGE] [--user USER_ID|all|current]
    ...
    Install an application.  Must provide the apk data to install, either as
    file path(s) or '-' to read from stdin.  Options are:
    ...
        --force-verification: if set, enable the verification for this install

    get-developer-verification-policy [--user USER_ID]
        Display current verification enforcement policy which will be applied to
        all the future installation sessions
            --user: show the policy of the given user (SYSTEM_USER if unspecified)

    get-developer-verification-service-provider
        Displays component name of developer verification service provider.
            --user: show the policy of the given user (SYSTEM_USER if unspecified)

    set-developer-verification-result PACKAGE POLICY RESULT [RESULT...]
        Set the developer verification enforcement policy and the result(s)
        in sequence for the next N verification sessions for the given app where
        N equals to the number of specified result(s).
            The valid verification policy values are:
            0 [none]: Do not block installs, regardless of verification result.
            1 [open]: Only block installs when verification fails.
            2 [warning]: The same as fail open.
            3 [closed]: Block installs when verification fails or cannot perform.
            The valid verification result values are:
            0 [invalid]: An invalid result value which will be skipped.
            1 [pass]: Verification passed.
            2 [reject]: Verification failed.
            3 [incomplete unknown]: Verification did not perform due to unknown.
            4 [incomplete network]: Verification did not perform due to network.
            5 [timeout]: Verification timed out.
            6 [disconnect]: Verification service disconnected.
            7 [infeasible]: Verification service was unavailable.

    clear-developer-verification-result [PACKAGE]
        Clear any previously set developer verification enforcement policy and
        result for the given app using set-developer-verification-result.
        If the package name is not specified, clear all previously set
        developer verification enforcement policy and results of future sessions

$ adb shell pm get-developer-verification-policy
0

$ adb shell pm get-developer-verification-service-provider
No verification service provider specified.

 

 

PackageManagerShellCommand

// com.android.server.pm.PackageManagerShellCommand

public class PackageManagerShellCommand extends ShellCommand {

    public final int runGetDeveloperVerificationPolicy() {
        PrintWriter pw = getOutPrintWriter();
        int userId = UserHandle.USER_ALL;
        while (true) {
            String opt = getNextOption();
            if (opt != null) {
                if (opt.equals("--user")) {
                    userId = UserHandle.parseUserArg(getNextArgRequired());
                    if (userId != UserHandle.USER_ALL && userId != UserHandle.USER_CURRENT &&
                        ((UserManagerInternal) LocalServices.getService(UserManagerInternal.class)).getUserInfo(userId) == null) {
                        pw.println("Failure [user " + userId + " doesn't exist]");
                        return 1;
                    }
                } else {
                    pw.println("Error: Unknown option: " + opt);
                    return 1;
                }
            } else {
                try {
                    pw.println(mInterface.getPackageInstaller().getDeveloperVerificationPolicy(
                        translateUserId(userId, UserHandle.USER_SYSTEM, "runGetDeveloperVerificationPolicy")));
                    return 0;
                } catch (Exception e) {
                    pw.println("Failure [" + e.getMessage() + "]");
                    return 1;
                }
            }
        }
    }

    public final int runGetDeveloperVerificationServiceProvider() {
        PrintWriter pw = getOutPrintWriter();
        try {
            ComponentName provider = mInterface.getPackageInstaller().getDeveloperVerificationServiceProvider();
            if (provider == null) {
                pw.println("No verification service provider specified.");
                return 0;
            }
            pw.println(provider.toString());
            return 0;
        } catch (Exception e) {
            pw.println("Failure [" + e.getMessage() + "]");
            return 1;
        }
    }

    public final int runSetDeveloperVerificationResult() {
        PrintWriter pw = getOutPrintWriter();
        try {
            IPackageInstaller packageInstaller = mInterface.getPackageInstaller();

            String packageName = getNextArgRequired();
            int policy = Integer.parseInt(getNextArgRequired());

            int result = Integer.parseInt(getNextArgRequired());
            ArrayList resultsList = new ArrayList(1);
            resultsList.add(Integer.valueOf(result));
            while (true) {
                String nextArg = getNextArg();
                if (nextArg == null) {
                    break;
                }
                resultsList.add(Integer.valueOf(Integer.parseInt(nextArg)));
            }
            int[] resultsArray = new int[resultsList.size()];
            for (int i = 0; i < resultsList.size(); i++) {
                resultsArray[i] = ((Integer) resultsList.get(i)).intValue();
            }

            packageInstaller.addDeveloperVerificationExperiment(packageName, policy, resultsArray);
            return 0;
        } catch (Exception e) {
            pw.println("Failure [" + e.getMessage() + "]");
            return 1;
        }
    }

    public final int runClearDeveloperVerificationResult() {
        PrintWriter pw = getOutPrintWriter();
        try {
            mInterface.getPackageInstaller().clearDeveloperVerificationExperiment(getNextArg());
            return 0;
        } catch (Exception e) {
            pw.println("Failure [" + e.getMessage() + "]");
            return 1;
        }
    }

}

 

 

PackageInstallerService

// com.android.server.pm.PackageInstallerService

public class PackageInstallerService extends IPackageInstaller.Stub implements PackageSessionProvider {

    public int getDeveloperVerificationPolicy(int userId) {
        int policy;

        int callingUid = IPackageInstaller.Stub.getCallingUid();
        if (mContext.checkPermission(Manifest.permission.DEVELOPER_VERIFICATION_AGENT, IPackageInstaller.Stub.getCallingPid(), callingUid) != 0 &&
            !isCallerDeveloperVerificationPolicyDelegate(mPm.snapshotComputer(), callingUid, userId)) {
            throw new SecurityException("Caller is not allowed to read the developer verification policy");
        }

        synchronized (mDeveloperVerificationPolicyPerUser) {
            try {
                if (mDeveloperVerificationPolicyPerUser.indexOfKey(userId) < 0) {
                    throw new IllegalStateException("Verification policy for user " + userId + " does not exist. Does the user exist?");
                }
                policy = mDeveloperVerificationPolicyPerUser.get(userId);
            } catch (Exception e) {
                throw e;
            }
        }

        return policy;
    }

    public boolean setDeveloperVerificationPolicy(int policy, int userId) {
        int callingUid = IPackageInstaller.Stub.getCallingUid();
        Computer snapshot = mPm.snapshotComputer();
        if (!isCallerDeveloperVerifier(snapshot, callingUid, userId) &&
            !isCallerDeveloperVerificationPolicyDelegate(snapshot, callingUid, userId)) {
            throw new SecurityException("Caller is not allowed to change the developer verification policy");
        }

        if (mDeveloperVerifierController.getVerifierPackageName() == null || !PackageInstallerSession.isValidVerificationPolicy(policy)) {
            return false;
        }

        synchronized (mDeveloperVerificationPolicyPerUser) {
            try {
                if (mDeveloperVerificationPolicyPerUser.indexOfKey(userId) < 0) {
                    throw new IllegalStateException("Verification policy for user " + userId + " does not exist. Does the user exist?");
                }
                if (policy != mDeveloperVerificationPolicyPerUser.get(userId)) {
                    mDeveloperVerificationPolicyPerUser.put(userId, policy);
                    mSettingsWriteRequest.schedule();
                }
            } catch (Exception e) {
                throw e;
            }
        }

        return true;
    }

    public ComponentName getDeveloperVerificationServiceProvider() {
        ComponentName verifierComponentName = mDeveloperVerifierController.getVerifierComponentName();
        if (verifierComponentName == null) {
            return null;
        }

        if (mPm.snapshotComputer().canQueryPackage(Binder.getCallingUid(), verifierComponentName.getPackageName())) {
            return verifierComponentName;
        }

        return null;
    }

    public String getDeveloperVerificationPolicyDelegatePackage(int userId) {
        PackageStateInternal packageStateFiltered;

        getDeveloperVerificationPolicyDelegatePackage_enforcePermission();

        Computer snapshot = mPm.snapshotComputer();
        int callingUid = IPackageInstaller.Stub.getCallingUid();
        if (!isCallerDeveloperVerifier(snapshot, callingUid, userId)) {
            throw new SecurityException("The caller is not the developer verifier and cannot query the policy delegate app.");
        }

        String developerVerificationPolicyDelegatePackageName = mPm.getDeveloperVerificationPolicyDelegatePackageName();
        if (developerVerificationPolicyDelegatePackageName == null ||
            (packageStateFiltered = snapshot.getPackageStateFiltered(developerVerificationPolicyDelegatePackageName, callingUid, i)) == null ||
            !packageStateFiltered.getUserStateOrDefault(userId).isInstalled()) {
            return null;
        }

        return packageStateFiltered.getPackageName();
    }

    public final boolean isCallerDeveloperVerifier(Computer snapshot, int callingUid, int userId) {
        String verifierPackageName = mDeveloperVerifierController.getVerifierPackageName();
        if (verifierPackageName == null) {
            return false;
        }

        return UserHandle.isSameApp(callingUid,
            snapshot.getPackageUidInternal(verifierPackageName, 0, userId, Process.SYSTEM_UID));
    }

    public final boolean isCallerDeveloperVerificationPolicyDelegate(Computer snapshot, int callingUid, int userId) {
        String developerVerificationPolicyDelegatePackageName = mPm.getDeveloperVerificationPolicyDelegatePackageName();
        if (developerVerificationPolicyDelegatePackageName == null) {
            return false;
        }

        return UserHandle.isSameApp(callingUid,
            snapshot.getPackageUidInternal(developerVerificationPolicyDelegatePackageName, 0, userId, Process.SYSTEM_UID));
    }
   
    public void setDeveloperVerificationUserResponse(int sessionId, int developerVerificationUserResponse) {
        setDeveloperVerificationUserResponse_enforcePermission();
        synchronized (mSessions) {
            try {
                PackageInstallerSession session = (PackageInstallerSession) mSessions.get(i);
                if (session != null) {
                    session.setVerificationUserResponse(developerVerificationUserResponse);
                } else {
                    Slog.e("PackageInstaller", "Session " + sessionId + " not found");
                }
            } catch (Exception e) {
                throw e;
            }
        }
    }

    public PackageInstaller.DeveloperVerificationUserConfirmationInfo getDeveloperVerificationUserConfirmationInfo(int sessionId) {
        PackageInstaller.DeveloperVerificationUserConfirmationInfo developerVerificationUserConfirmationInfo;
        getDeveloperVerificationUserConfirmationInfo_enforcePermission();
        synchronized (mSessions) {
            try {
                PackageInstallerSession session = (PackageInstallerSession) mSessions.get(sessionId);
                developerVerificationUserConfirmationInfo = session != null ?
                    session.generateDeveloperVerificationUserConfirmationInfo() : null;
            } catch (Exception e) {
                throw e;
            }
        }
        return developerVerificationUserConfirmationInfo;
    }

    public void addDeveloperVerificationExperiment(String packageName, int policy, int[] statusArray) {
        ArrayList statusList = new ArrayList(statusArray.length);
        for (int status : statusArray) {
            statusList.add(Integer.valueOf(status));
        }

        mDeveloperVerifierController.addExperiment(packageName, policy, statusList);
    }

    public void clearDeveloperVerificationExperiment(String packageName) {
        mDeveloperVerifierController.clearExperiment(packageName);
    }

    public void onUserAdded(int userId) {
        synchronized (mDeveloperVerificationPolicyPerUser) {
            mDeveloperVerificationPolicyPerUser.put(userId, PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE);
        }
    }

    public void onUserRemoved(int i) {
        synchronized (mDeveloperVerificationPolicyPerUser) {
            mDeveloperVerificationPolicyPerUser.delete(userId);
        }
    }

}

 

 

PackageInstallerSession

// com.android.server.pm.PackageInstallerSession

public class PackageInstallerSession extends IPackageInstallerSession.Stub {

    public final void handleInstall() {
        ...

        if (isVerificationServiceEnabled()) {
            performDeveloperVerification();
        } else {
            resumeVerify();
        }
    }

    public final void performDeveloperVerification() {
        ...
        completableFuture = new CompletableFuture();
        startDeveloperVerificationSession(completableFuture, false);

        completableFuture.thenAccept(new Consumer() {
            @Override 
            public final void accept(Object obj) {
                processDeveloperVerificationResults((PackageInstallerSession.DeveloperVerificationFutureResult) obj);
            }
        });
    }

    public final void processDeveloperVerificationResults(DeveloperVerificationFutureResult result) {
        if (result.mSuccess) {
            resumeVerify();
        } else {
            setSessionFailedDueToDeveloperVerification(result.mVerificationFailureReason, true, result.mFailedMessage);
        }
    }

    public final void startDeveloperVerificationSession(CompletableFuture<InstallResult> future, boolean retry) {
        SigningInfo signingInfo;
        List<SharedLibraryInfo> declaredLibraries;
        String packageName = getPackageName();

        DeveloperVerifierCallback developerVerifierCallback = new DeveloperVerifierCallback(future);
        if (mDeveloperVerifierController.hasExperiments(packageName)) {
            mDeveloperVerifierController.startLocalExperiment(packageName, developerVerifierCallback);
            synchronized (mMetrics) {
                mMetrics.onDeveloperVerificationBypassed(DeveloperVerificationSession.DEVELOPER_VERIFICATION_BYPASSED_REASON_TEST);
            }
            return;
        }

        if (!shouldUseVerificationService()) {
            developerVerifierCallback.onDeveloperVerificationSkipped();
            return;
        }

        synchronized (mLock) {
            signingInfo = new SigningInfo(mSigningDetails);
            PackageLite packageLite = mPackageLite;
            declaredLibraries = packageLite == null ? null : packageLite.getDeclaredLibraries();
        }

        DeveloperVerifierController developerVerifierController = mDeveloperVerifierController;
        PackageManagerService packageManagerService = mPm;
        Objects.requireNonNull(packageManagerService);
        if (!developerVerifierController.startVerificationSession(
            new DistractingPackageHelper(packageManagerService), userId, sessionId, packageName,
                Uri.fromFile(stageDir), signingInfo, declaredLibraries,
                mCurrentVerificationPolicy.get(), params.extensionParams,
                developerVerifierCallback, new Runnable(this), retry)) {
            developerVerifierCallback.onConnectionInfeasible();
            return;
        }

        synchronized (mMetrics) {
            try {
                if (!retry) {
                    mMetrics.onDeveloperVerificationRequestSent();
                } else {
                    mMetrics.onDeveloperVerificationRetryRequestSent();
                }
            } finally {
            }
        }
    }

    public final boolean isVerificationServiceEnabled() {
        return true;
    }

    public final boolean shouldUseVerificationService() {
        if (!isVerificationServiceEnabled() || mDeveloperVerifierController.getVerifierPackageName() == null) {
            return false;
        }
        PackageInstaller.SessionParams sessionParams = params;
        if ((sessionParams.installFlags & PackageManager.INSTALL_FROM_ADB) == 0 || sessionParams.forceVerification) {
            return true;
        }
        synchronized (mMetrics) {
            mMetrics.onDeveloperVerificationBypassed(DeveloperVerificationSession.DEVELOPER_VERIFICATION_BYPASSED_REASON_ADB);
        }
        return false;
    }

    public boolean shouldAllowDeveloperVerificationEmergencyBypass(String packageName) {
        Computer computerSnapshotComputer;
        String installerPackageName;
        String systemAppUpdateOwnerPackageName;
        PackageStateInternal packageState;
        PackageStateInternal systemAppUpdateOwnerPackageState;
        PackageStateInternal emergencyInstallerPackageState;
        String verifierPackageName = mDeveloperVerifierController.getVerifierPackageName();
        if (verifierPackageName == null || packageName == null ||
            (packageState = (computerSnapshotComputer = mPm.snapshotComputer()).getPackageStateInternal(packageName, Process.SYSTEM_UID)) == null ||
            !packageState.isSystem() ||
            (systemAppUpdateOwnerPackageName = mPm.getSystemAppUpdateOwnerPackageName(verifierPackageName)) == null ||
            (installerPackageName = getInstallerPackageName()) == null ||
            (systemAppUpdateOwnerPackageState = computerSnapshotComputer.getPackageStateInternal(systemAppUpdateOwnerPackageName, Process.SYSTEM_UID)) == null ||
            systemAppUpdateOwnerPackageState.getPkg() == null ||
            !hasSystemInstallerPermissions(computerSnapshotComputer, systemAppUpdateOwnerPackageState.getAppId())) {
            return false;
        }

        if (TextUtils.equals(verifierPackageName, packageName) && TextUtils.equals(systemAppUpdateOwnerPackageName, installerPackageName)) {
            Slog.d("PackageInstallerSession", "Bypassing developer verification because the verifier is being updated.");
            return true;
        }

        if (TextUtils.equals(systemAppUpdateOwnerPackageName, packageName) && TextUtils.equals(systemAppUpdateOwnerPackageName, installerPackageName)) {
            Slog.d("PackageInstallerSession", "Bypassing developer verification because the update-owner of the verifier which is specified in the sysconfig is being updated.");
            return true;
        }

        String emergencyInstallerPackageName = systemAppUpdateOwnerPackageState.getPkg().getEmergencyInstaller();
        if (emergencyInstallerPackageName == null ||
            (emergencyInstallerPackageState = computerSnapshotComputer.getPackageStateInternal(emergencyInstallerPackageName, Process.SYSTEM_UID)) == null ||
            emergencyInstallerPackageState.getPkg() == null ||
            !hasEmergencyInstallerPermission(computerSnapshotComputer, emergencyInstallerPackageState.getAppId())) {
            return false;
        }

        if (TextUtils.equals(emergencyInstallerPackageName, packageName) && TextUtils.equals(systemAppUpdateOwnerPackageName, installerPackageName)) {
            Slog.d("PackageInstallerSession", "Bypassing developer verification because the emergency installer of the update-owner of the verifier which is specified in the sysconfig is being updated.");
            return true;
        }

        if (TextUtils.equals(systemAppUpdateOwnerPackageName, packageName) && TextUtils.equals(emergencyInstallerPackageName, installerPackageName)) {
            Slog.d("PackageInstallerSession", "Bypassing developer verification because the update-owner of the verifier which is specified in the sysconfig is being updated by its emergency installer.");
            return true;
        }

        return false;
    }

    public String getInstallerPackageName() {
        return getInstallSource().mInstallerPackageName;
    }



    public class DeveloperVerifierCallback {
        public final CompletableFuture mFuture;

        public void onDeveloperVerificationSkipped() {
            PackageInstallerSession.this.mHandler.post(() -> {
                mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
            });
        }

        public boolean onVerificationPolicyOverridden(int i) {
            if (!PackageInstallerSession.isValidVerificationPolicy(i)) {
                return false;
            }
            PackageInstallerSession.this.mCurrentVerificationPolicy.set(i);
            synchronized (PackageInstallerSession.this.mMetrics) {
                PackageInstallerSession.this.mMetrics.onDeveloperVerificationPolicyOverridden(i);
            }
            return true;
        }

        public void onConnectionInfeasible() {
            PackageInstallerSession.this.mHandler.post(() -> {
                PackageInstallerSession.this.mDeveloperVerificationStatusInternal.setInternalStatus(7);
                synchronized (PackageInstallerSession.this.mMetrics) {
                    PackageInstallerSession.this.mMetrics.onDeveloperVerificationFinished(PackageInstallerSession.this.mDeveloperVerificationStatusInternal);
                }

                if (PackageInstallerSession.this.mCurrentVerificationPolicy.get() == PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE) {
                    mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
                    return;
                }

                PackageInstallerSession session = PackageInstallerSession.this;
                if (session.shouldAllowDeveloperVerificationEmergencyBypass(session.getPackageName())) {
                    setDeveloperVerificationBypassedForEmergency();
                } else if (PackageInstallerSession.this.shouldSendUserActionForVerification(false)) {
                    PackageInstallerSession.this.sendDeveloperVerificationUserAction(DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN, mFuture);
                } else {
                    mFuture.complete(DeveloperVerificationFutureResult.ofFailure(PackageInstaller.DEVELOPER_VERIFICATION_FAILED_REASON_UNKNOWN,
                        "A verifier agent is specified on device but cannot be connected because of unknown error."));
                }
            });
        }

        public void onConnectionFailed() {
            PackageInstallerSession.this.mDeveloperVerificationStatusInternal.setInternalStatus(6);
            synchronized (PackageInstallerSession.this.mMetrics) {
                PackageInstallerSession.this.mMetrics.onDeveloperVerificationFinished(PackageInstallerSession.this.mDeveloperVerificationStatusInternal);
            }

            if (PackageInstallerSession.this.mCurrentVerificationPolicy.get() == PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE) {
                mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
                return;
            }

            PackageInstallerSession session = PackageInstallerSession.this;
            if (session.shouldAllowDeveloperVerificationEmergencyBypass(session.getPackageName())) {
                setDeveloperVerificationBypassedForEmergency();
            } else if (PackageInstallerSession.this.shouldSendUserActionForVerification(false)) {
                PackageInstallerSession.this.sendDeveloperVerificationUserAction(DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN, mFuture);
            } else {
                mFuture.complete(DeveloperVerificationFutureResult.ofFailure(PackageInstaller.DEVELOPER_VERIFICATION_FAILED_REASON_UNKNOWN,
                    "A verifier agent is available on device but cannot be connected."));
            }
        }

        public void onTimeoutExtensionRequested() {
            synchronized (PackageInstallerSession.this.mMetrics) {
                PackageInstallerSession.this.mMetrics.onDeveloperVerificationTimeoutExtensionRequested();
            }
        }

        public void onTimeout() {
            DeveloperVerifierController developerVerifierController = PackageInstallerSession.this.mDeveloperVerifierController;
            PackageInstallerSession session = PackageInstallerSession.this;
            developerVerifierController.notifyVerificationTimeout(session.sessionId, session.userId);
            PackageInstallerSession.this.mHandler.post(() -> {
                PackageInstallerSession.this.mDeveloperVerificationStatusInternal.setInternalStatus(5);
                synchronized (PackageInstallerSession.this.mMetrics) {
                    PackageInstallerSession.this.mMetrics.onDeveloperVerificationFinished(PackageInstallerSession.this.mDeveloperVerificationStatusInternal);
                }

                if (PackageInstallerSession.this.mCurrentVerificationPolicy.get() == PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE) {
                    mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
                    return;
                }

                PackageInstallerSession session = PackageInstallerSession.this;
                if (session.shouldAllowDeveloperVerificationEmergencyBypass(session.getPackageName())) {
                    setDeveloperVerificationBypassedForEmergency();
                } else if (PackageInstallerSession.this.shouldSendUserActionForVerification(false)) {
                    PackageInstallerSession.this.sendDeveloperVerificationUserAction(DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN, mFuture);
                } else {
                    mFuture.complete(DeveloperVerificationFutureResult.ofFailure(PackageInstaller.DEVELOPER_VERIFICATION_FAILED_REASON_UNKNOWN,
                        "Verification timed out; missing a response from the verifier within the time limit"));
                }
            });
        }

        public void onVerificationCompleteReceived(final DeveloperVerificationStatus developerVerificationStatus, final PersistableBundle persistableBundle) {
            PackageInstallerSession.this.mHandler.post(() -> {
                PackageInstallerSession.this.mDeveloperVerificationStatusInternal.setInternalStatus(developerVerificationStatus.isVerified() ? 1 : 2);
                PackageInstallerSession.this.mDeveloperVerificationStatusInternal.setAppMetadataVerificationStatus(developerVerificationStatus.getAppMetadataVerificationStatus());
                PackageInstallerSession.this.mDeveloperVerificationStatusInternal.setLiteVerification(developerVerificationStatus.isLiteVerification());
                PackageInstallerSession.this.mDeveloperVerificationExtensionResponse = persistableBundle;
                
                synchronized (PackageInstallerSession.this.mMetrics) {
                    PackageInstallerSession.this.mMetrics.onDeveloperVerificationFinished(PackageInstallerSession.this.mDeveloperVerificationStatusInternal);
                }

                if (PackageInstallerSession.this.mCurrentVerificationPolicy.get() == PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE) {
                    mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
                    return;
                }

                if (developerVerificationStatus.isVerified()) {
                    mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
                    return;
                }

                PackageInstallerSession session = PackageInstallerSession.this;
                if (session.shouldAllowDeveloperVerificationEmergencyBypass(session.getPackageName())) {
                    setDeveloperVerificationBypassedForEmergency();
                    return;
                }

                if (PackageInstallerSession.this.shouldSendUserActionForVerification(true)) {
                    PackageInstallerSession.this.sendDeveloperVerificationUserAction(DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_DEVELOPER_BLOCKED, mFuture);
                    return;
                }

                StringBuilder builder = new StringBuilder("Verifier rejected the installation");
                if (!TextUtils.isEmpty(developerVerificationStatus.getFailureMessage())) {
                    builder.append(" with message: ");
                    builder.append(developerVerificationStatus.getFailureMessage());
                }
                mFuture.complete(DeveloperVerificationFutureResult.ofFailure(PackageInstaller.DEVELOPER_VERIFICATION_FAILED_REASON_DEVELOPER_BLOCKED, builder.toString()));
            });
        }

        public void onVerificationIncompleteReceived(final int verificationFailureReason) {
            PackageInstallerSession.this.mHandler.post(() -> {
                int userActionNeededReason = DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_NETWORK_UNAVAILABLE;
                boolean networkUnavailable = verificationFailureReason == PackageInstaller.DEVELOPER_VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE;

                PackageInstallerSession.this.mDeveloperVerificationStatusInternal.setInternalStatus(networkUnavailable ? 4 : 3);
                synchronized (PackageInstallerSession.this.mMetrics) {
                    PackageInstallerSession.this.mMetrics.onDeveloperVerificationFinished(PackageInstallerSession.this.mDeveloperVerificationStatusInternal);
                }

                if (PackageInstallerSession.this.mCurrentVerificationPolicy.get() == PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE) {
                    mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
                    return;
                }

                PackageInstallerSession session = PackageInstallerSession.this;
                if (session.shouldAllowDeveloperVerificationEmergencyBypass(session.getPackageName())) {
                    setDeveloperVerificationBypassedForEmergency();
                    return;
                }

                StringBuilder builder = new StringBuilder("Verification cannot be completed because of ");
                if (networkUnavailable) {
                    builder.append("unavailable network.");
                } else {
                    builder.append("unknown reasons.");
                    userActionNeededReason = DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN;
                }

                if (PackageInstallerSession.this.shouldSendUserActionForVerification(false)) {
                    PackageInstallerSession.this.sendDeveloperVerificationUserAction(userActionNeededReason, mFuture);
                } else {
                    mFuture.complete(DeveloperVerificationFutureResult.ofFailure(userActionNeededReason, builder.toString()));
                }
            });
        }

        public void onVerificationBypassedReceived(int bypassReason) {
            synchronized (PackageInstallerSession.this.mMetrics) {
                PackageInstallerSession.this.mMetrics.onDeveloperVerificationFinished(PackageInstallerSession.this.mDeveloperVerificationStatusInternal);
                PackageInstallerSession.this.mMetrics.onDeveloperVerificationBypassed(bypassReason);
            }
            PackageInstallerSession.this.mHandler.post(() -> {
                mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
            });
        }

        public final void setDeveloperVerificationBypassedForEmergency() {
            synchronized (PackageInstallerSession.this.mMetrics) {
                PackageInstallerSession.this.mMetrics.onDeveloperVerificationBypassed(DeveloperVerificationSession.DEVELOPER_VERIFICATION_BYPASSED_REASON_EMERGENCY);
            }
            mFuture.complete(DeveloperVerificationFutureResult.ofSuccess());
        }
    }

    public final boolean shouldSendUserActionForVerification(boolean verificationCompleted) {
        Computer computerSnapshotComputer = this.mPm.snapshotComputer();
        String installerPackageName = getInstallerPackageName();
        if (installerPackageName == null) {
            return false;
        }

        ApplicationInfo applicationInfo = computerSnapshotComputer.getApplicationInfo(installerPackageName, 0L, this.userId);
        if (applicationInfo == null) {
            Log.w("PackageInstallerSession", "Could not find ApplicationInfo for " + installerPackageName);
            return false;
        }

        if (installerPackageName.equals(this.mPm.getPackageInstallerPackageName())) {
            return true;
        }

        return applicationInfo.targetSdkVersion > 36 ? !verificationCompleted :
            computerSnapshotComputer.checkUidPermission("android.permission.INSTALL_PACKAGES", applicationInfo.uid) != 0;
    }

    public final void sendDeveloperVerificationUserAction(int userActionNeededReason, CompletableFuture completableFuture) {
        IntentSender receiver;
        mDeveloperVerificationUserActionState = new DeveloperVerificationUserActionState(userActionNeededReason, completableFuture);
        Intent developerVerificationUserActionIntent = getDeveloperVerificationUserActionIntent();

        if (hasParentSessionId()) {
            receiver = mSessionProvider.getSession(getParentSessionId()).getRemoteStatusReceiver();
        } else {
            receiver = getRemoteStatusReceiver();
        }

        sendOnUserActionRequired(mContext, receiver, sessionId, developerVerificationUserActionIntent);
        synchronized (mMetrics) {
            mMetrics.onDeveloperVerificationUserActionRequired(userActionNeededReason);
        }
    }

    public final void setSessionFailedDueToDeveloperVerification(int verificationFailureReason, boolean addExtraIntent, String failedMessage) {
        Bundle bundle = new Bundle();
        bundle.putInt(PackageInstaller.EXTRA_DEVELOPER_VERIFICATION_FAILURE_REASON, verificationFailureReason);
        bundle.putBoolean(PackageInstaller.EXTRA_DEVELOPER_VERIFICATION_LITE_PERFORMED, mDeveloperVerificationStatusInternal.isLiteVerification());
        if (addExtraIntent) {
            Intent extraIntent = getDeveloperVerificationUserActionIntent();
            extraIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, getPackageName());
            extraIntent.putExtra(PackageInstaller.EXTRA_DEVELOPER_VERIFICATION_FAILURE_REASON, verificationFailureReason);
            bundle.putParcelable(Intent.EXTRA_INTENT, extraIntent);
        }
        setSessionFailed(PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE, failedMessage);
        onSessionVerificationFailure(PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE, failedMessage, bundle);
    }

     public final Intent getDeveloperVerificationUserActionIntent() {
        return new Intent(PackageInstaller.ACTION_CONFIRM_DEVELOPER_VERIFICATION).setPackage(
            mPm.getPackageInstallerPackageName()).putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
    }

}

 

 

DeveloperVerifierController

// com.android.server.pm.verify.developer.DeveloperVerifierController

public class DeveloperVerifierController {

    public final ComponentName mDeveloperVerificationServiceProvider;
    public final DeveloperVerifierExperimentProvider mExperimentProvider;



    public String getVerifierPackageName() {
        ComponentName componentName = mDeveloperVerificationServiceProvider;
        if (componentName == null) {
            return null;
        }
        return componentName.getPackageName();
    }



    public boolean startVerificationSession(Supplier supplier, int userId, final int installSessionId,
        String packageName, Uri stagedPackageUri,
        SigningInfo signingInfo, List<SharedLibraryInfo> declaredLibraries,
        int defaultPolicy, PersistableBundle extensionParams,
        final PackageInstallerSession.DeveloperVerifierCallback developerVerifierCallback,
        Runnable runnable, final boolean retry) {

        synchronized (mRemoteServices) {
            try {
                ServiceConnectorWrapper serviceConnectorWrapper = (ServiceConnectorWrapper) mRemoteServices.get(userId);
                if (serviceConnectorWrapper == null) {
                    return false;
                }
                DeveloperVerificationSession session = new DeveloperVerificationSession(
                    installSessionId, installSessionId, packageName, stagedPackageUri, signingInfo, declaredLibraries,
                    extensionParams, defaultPolicy, new DeveloperVerificationSessionInterface(developerVerifierCallback));

                serviceConnectorWrapper.getService().post(new ServiceConnector.VoidJob() {
                    public final void runNoResult(Object service) {
                        if (!retry) {
                            service.onVerificationRequired(session);
                        } else {
                            service.onVerificationRetry(session);
                        }
                    }
                }).orTimeout(mInjector.getVerifierConnectionTimeoutMillis(), TimeUnit.MILLISECONDS).whenComplete(new BiConsumer() {
                    public final void accept(Object service, Throwable t) {
                        Slog.e("VerifierController", "Error notifying verification request for session " + installSessionId, t);
                        developerVerifierCallback.onConnectionFailed();
                    }
                });

            } finally {
            }
        }
    }



    public void addExperiment(String packageName, int policy, List<Integer> status) {
        mExperimentProvider.addExperiment(packageName, policy, status);
    }

    public void clearExperiment(String packageName) {
        mExperimentProvider.clearExperiment(packageName);
    }

    public boolean hasExperiments(String packageName) {
        return mExperimentProvider.hasExperiments(packageName);
    }

    public void startLocalExperiment(String packageName, PackageInstallerSession.DeveloperVerifierCallback developerVerifierCallback) {
        mExperimentProvider.runNextExperiment(packageName, developerVerifierCallback);
    }



    public class DeveloperVerificationSessionInterface implements IDeveloperVerificationSessionInterface {

    }

}

 

 

IDeveloperVerifierService

// android.content.pm.verify.developer.IDeveloperVerifierService

public interface IDeveloperVerifierService extends IInterface {

    void onPackageNameAvailable(String packageName) throws RemoteException;

    void onVerificationCancelled(String packageName) throws RemoteException;

    void onVerificationRequired(DeveloperVerificationSession session) throws RemoteException;

    void onVerificationRetry(DeveloperVerificationSession session) throws RemoteException;

    void onVerificationTimeout(int verificationId) throws RemoteException;

}

 

 

IDeveloperVerificationSessionInterface

// android.content.pm.verify.developer.IDeveloperVerificationSessionInterface

public interface IDeveloperVerificationSessionInterface extends IInterface {

    long getTimeoutTimeMillis(int verificationId) throws RemoteException;

    long extendTimeoutMillis(int verificationId, long additionalMillis) throws RemoteException;

    boolean setVerificationPolicy(int verificationId, int policy) throws RemoteException;

    void reportVerificationIncomplete(int verificationId, int verificationFailureReason) throws RemoteException;

    void reportVerificationComplete(int verificationId, DeveloperVerificationStatus status, PersistableBundle extensionResponse) throws RemoteException;

    void reportVerificationBypassed(int verificationId, int bypassReason) throws RemoteException;

}

 

 

DeveloperVerifierService

// android.content.pm.verify.developer.DeveloperVerifierService

public abstract class DeveloperVerifierService extends Service {

    public abstract void onPackageNameAvailable(String packageName);

    public abstract void onVerificationCancelled(String packageName);

    public abstract void onVerificationRequired(DeveloperVerificationSession session);

    public abstract void onVerificationRetry(DeveloperVerificationSession session);

    public abstract void onVerificationTimeout(int verificationId);



    public IBinder onBind(Intent intent) {
        if (intent == null || !PackageManager.ACTION_VERIFY_DEVELOPER.equals(intent.getAction())) {
            return null;
        }

        return new IDependencyInstallerService.Stub() {
            @Override
            public void onPackageNameAvailable(String packageName) {
                DeveloperVerifierService.this.onPackageNameAvailable(packageName);
            }

            @Override
            public void onVerificationCancelled(String packageName) {
                DeveloperVerifierService.this.onVerificationCancelled(packageName);
            }

            @Override
            public void onVerificationRequired(DeveloperVerificationSession session) {
                DeveloperVerifierService.this.onVerificationRequired(session);
            }

            @Override
            public void onVerificationRetry(DeveloperVerificationSession session) {
                DeveloperVerifierService.this.onVerificationRetry(session);
            }

            @Override
            public void onVerificationTimeout(int verificationId) {
                DeveloperVerifierService.this.onVerificationTimeout(verificationId);
            }
        };
    }

}

 

 

DeveloperVerificationSession

// android.content.pm.verify.developer.DeveloperVerificationSession

public final class DeveloperVerificationSession implements Parcelable {

    public static final int DEVELOPER_VERIFICATION_BYPASSED_REASON_ADB = 1;
    public static final int DEVELOPER_VERIFICATION_BYPASSED_REASON_EMERGENCY = 2;
    public static final int DEVELOPER_VERIFICATION_BYPASSED_REASON_TEST = 3;
    public static final int DEVELOPER_VERIFICATION_BYPASSED_REASON_UNSPECIFIED = 0;
    public static final int DEVELOPER_VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1;
    public static final int DEVELOPER_VERIFICATION_INCOMPLETE_UNKNOWN = 0;

    private final List<SharedLibraryInfo> mDeclaredLibraries;
    private final PersistableBundle mExtensionParams;
    private final int mId;
    private final int mInstallSessionId;
    private final String mPackageName;
    private volatile int mPolicy;
    private final IDeveloperVerificationSessionInterface mSession;
    private final SigningInfo mSigningInfo;
    private final Uri mStagedPackageUri;

    public DeveloperVerificationSession(int id, int installSessionId,
        String packageName, Uri stagedPackageUri,
        SigningInfo signingInfo,
        List<SharedLibraryInfo> declaredLibraries,
        PersistableBundle extensionParams, int defaultPolicy,
        IDeveloperVerificationSessionInterface session) {
        this.mId = id;
        this.mInstallSessionId = installSessionId;
        this.mPackageName = packageName;
        this.mStagedPackageUri = stagedPackageUri;
        this.mSigningInfo = signingInfo;
        this.mDeclaredLibraries = declaredLibraries;
        this.mExtensionParams = extensionParams;
        this.mPolicy = defaultPolicy;
        this.mSession = session;
    }

}

 

 

PackageManagerService

// com.android.server.pm.PackageManagerService

public class PackageManagerService implements PackageSender, TestUtilityService {

    public PackageManagerService(...) {
        ComponentName mDeveloperVerificationServiceProvider = getVerificationServiceProvider(
            snapshotLocked, mContext.getResources().getString(R.string.config_developerVerificationServiceProviderPackageName));
        String mDeveloperVerificationPolicyDelegatePackage = getVerificationPolicyDelegate(
            snapshotLocked, mContext.getResources().getString(R.string.config_developerVerificationPolicyDelegatePackageName));
    }

    public static ComponentName getVerificationServiceProvider(Computer snapshot, String providerPackageName) {
        if (TextUtils.isEmpty(providerPackageName)) {
            return null;
        }

        Intent intent = new Intent(PackageManager.ACTION_VERIFY_DEVELOPER);
        intent.setPackage(providerPackageName);
        List listQueryIntentServicesInternal = snapshot.queryIntentServicesInternal(
            intent, /* resolvedType */ null, /* resolveFlags */ 1835520L, UserHandle.USER_SYSTEM, Process.myUid(),
            Process.INVALID_PID,  /* includeInstantApps */ false,  /* resolveForStart */ false);
        if (listQueryIntentServicesInternal.isEmpty()) {
            return null;
        }

        ResolveInfo resolveInfo = (ResolveInfo) listQueryIntentServicesInternal.getFirst();
        if (resolveInfo.getComponentInfo() == null || resolveInfo.getComponentInfo().applicationInfo == null ||
            snapshot.checkUidPermission(Manifest.permission.DEVELOPER_VERIFICATION_AGENT, resolveInfo.getComponentInfo().applicationInfo.uid) != 0) {
            return null;
        }

        return resolveInfo.getComponentInfo().getComponentName();
    }

    public final String getVerificationPolicyDelegate(Computer snapshot, String delegatePackageName) {
        PackageStateInternal packageState;

        if (!TextUtils.isEmpty(delegatePackageName) &&
            (packageState = snapshot.getPackageStateInternal(delegatePackageName, Process.SYSTEM_UID)) != null &&
                packageState.isSystem() && packageState.isPrivileged()) {
            return delegatePackageName;
        }

        return null;
    }

}

 

 

PackageManager

// android.content.pm.PackageManager

public abstract class PackageManager {

    @SystemApi
    public static final String ACTION_VERIFY_DEVELOPER = "android.content.pm.action.VERIFY_DEVELOPER";

    @SystemApi
    public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22;

}

 

 

IPackageInstaller

// android.content.pm.IPackageInstaller

public interface IPackageInstaller extends IInterface {

    int getDeveloperVerificationPolicy(int userId) throws RemoteException;

    boolean setDeveloperVerificationPolicy(int policy, int userId) throws RemoteException;

    ComponentName getDeveloperVerificationServiceProvider() throws RemoteException;

    String getDeveloperVerificationPolicyDelegatePackage(int userId) throws RemoteException;

    void setDeveloperVerificationUserResponse(int sessionId, int developerVerificationUserResponse) throws RemoteException;

    PackageInstaller.DeveloperVerificationUserConfirmationInfo getDeveloperVerificationUserConfirmationInfo(int sessionId) throws RemoteException;

    void addDeveloperVerificationExperiment(String packageName, int verificationPolicy, int[] results) throws RemoteException;

    void clearDeveloperVerificationExperiment(String packageName) throws RemoteException;



    public static abstract class Stub extends Binder implements IPackageInstaller {
        protected void getDeveloperVerificationPolicyDelegatePackage_enforcePermission() throws SecurityException {
            this.mEnforcer.enforcePermission(Manifest.permission.DEVELOPER_VERIFICATION_AGENT, getCallingPid(), getCallingUid());
        }

        protected void setDeveloperVerificationUserResponse_enforcePermission() throws SecurityException {
            this.mEnforcer.enforcePermission(Manifest.permission.SET_DEVELOPER_VERIFICATION_USER_RESPONSE, getCallingPid(), getCallingUid());
        }

        protected void getDeveloperVerificationUserConfirmationInfo_enforcePermission() throws SecurityException {
            this.mEnforcer.enforcePermission(Manifest.permission.SET_DEVELOPER_VERIFICATION_USER_RESPONSE, getCallingPid(), getCallingUid());
        }
    }

}

 

 

PackageInstaller

// android.content.pm.PackageInstaller

public class PackageInstaller {

    @SystemApi
    public static final String ACTION_CONFIRM_DEVELOPER_VERIFICATION = "android.content.pm.action.CONFIRM_DEVELOPER_VERIFICATION";

    public static final int DEVELOPER_VERIFICATION_FAILED_REASON_DEVELOPER_BLOCKED = 2;
    public static final int DEVELOPER_VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE = 1;
    public static final int DEVELOPER_VERIFICATION_FAILED_REASON_UNKNOWN = 0;

    @SystemApi
    public static final int DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_CLOSED = 3;
    @SystemApi
    public static final int DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_OPEN = 1;
    @SystemApi
    public static final int DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_WARN = 2;
    @SystemApi
    public static final int DEVELOPER_VERIFICATION_POLICY_NONE = 0;

    @SystemApi
    public static final int DEVELOPER_VERIFICATION_USER_RESPONSE_ABORT = 1;
    @SystemApi
    public static final int DEVELOPER_VERIFICATION_USER_RESPONSE_ERROR = 0;
    @SystemApi
    public static final int DEVELOPER_VERIFICATION_USER_RESPONSE_INSTALL_ANYWAY = 3;
    @SystemApi
    public static final int DEVELOPER_VERIFICATION_USER_RESPONSE_RETRY = 2;

    public static final String EXTRA_DEVELOPER_VERIFICATION_EXTENSION_RESPONSE = "android.content.pm.extra.DEVELOPER_VERIFICATION_EXTENSION_RESPONSE";
    public static final String EXTRA_DEVELOPER_VERIFICATION_FAILURE_REASON = "android.content.pm.extra.DEVELOPER_VERIFICATION_FAILURE_REASON";
    public static final String EXTRA_DEVELOPER_VERIFICATION_LITE_PERFORMED = "android.content.pm.extra.DEVELOPER_VERIFICATION_LITE_PERFORMED";

    public static final String EXTRA_PACKAGE_NAME = "android.content.pm.extra.PACKAGE_NAME";

    public static final String EXTRA_SESSION_ID = "android.content.pm.extra.SESSION_ID";

    private final IPackageInstaller mInstaller;

    @SystemApi
    public final int getDeveloperVerificationPolicy() {
        try {
            return mInstaller.getDeveloperVerificationPolicy(this.mUserId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @SystemApi
    public final boolean setDeveloperVerificationPolicy(int policy) {
        try {
            return mInstaller.setDeveloperVerificationPolicy(policy, this.mUserId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    public final ComponentName getDeveloperVerificationServiceProvider() {
        try {
            return mInstaller.getDeveloperVerificationServiceProvider();
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @SystemApi
    public String getDeveloperVerificationPolicyDelegatePackage() {
        try {
            return mInstaller.getDeveloperVerificationPolicyDelegatePackage(this.mUserId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @SystemApi
    public void setDeveloperVerificationUserResponse(int sessionId, int developerVerificationUserResponse) {
        try {
            mInstaller.setDeveloperVerificationUserResponse(sessionId, developerVerificationUserResponse);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    @SystemApi
    public DeveloperVerificationUserConfirmationInfo getDeveloperVerificationUserConfirmationInfo(int sessionId) {
        try {
            return mInstaller.getDeveloperVerificationUserConfirmationInfo(sessionId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }


    // Following are not implemented here and requires going through `PackageManagerShellCommand`.
    //void addDeveloperVerificationExperiment(String packageName, int verificationPolicy, int[] results);
    //void clearDeveloperVerificationExperiment(String packageName);



    public static final class DeveloperVerificationUserConfirmationInfo implements Parcelable {

        public static final int DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_DEVELOPER_BLOCKED = 2;
        public static final int DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_LITE_VERIFICATION = 3;
        public static final int DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_NETWORK_UNAVAILABLE = 1;
        public static final int DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN = 0;

        private final int mUserActionNeededReason;
        private final int mVerificationPolicy;

        public DeveloperVerificationUserConfirmationInfo(int policy, int reason) {
            this.mVerificationPolicy = policy;
            this.mUserActionNeededReason = reason;
        }

    }

}

 

 

core/res/AndroidManifest.xml

// core/res/AndroidManifest.xml

<permission
    android:name="android.permission.DEVELOPER_VERIFICATION_AGENT"
    android:protectionLevel="signature|privileged"
    android:featureFlag="android.content.pm.verification_service" />

<permission
    android:name="android.permission.BIND_DEVELOPER_VERIFICATION_AGENT"
    android:protectionLevel="internal"
    android:featureFlag="android.content.pm.verification_service"/>

<permission
    android:name="android.permission.SET_DEVELOPER_VERIFICATION_USER_RESPONSE"
    android:protectionLevel="installer|internal"
    android:featureFlag="android.content.pm.verification_service"/>

 

 

GooglePackageInstaller

The GooglePackageInstaller app (com.google.android.packageinstaller) implements the Package Installer app used for showing user prompts. On Android 16 QPR2 Beta 3, it is still using targetSdkVersion = 36, but is not handling developer verification prompts as verification is not enabled.

It handles the PackageInstaller.ACTION_CONFIRM_DEVELOPER_VERIFICATION when PackageInstallerSession.sendDeveloperVerificationUserAction() is called if verification policy is not set PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE.

// res/AndroidManifest.xml

<activity
    android:name="com.android.packageinstaller.InstallStart"
    android:exported="true">
    <intent-filter android:priority="1">
        <action android:name="android.content.pm.action.CONFIRM_DEVELOPER_VERIFICATION"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>
// com.android.packageinstaller.v2.ui.fragments.InstallationFragment

public class InstallationFragment extends DialogFragment {

    public TextView mInstallWithoutVerifyingTextView = null;

    public static int getVerificationConfirmationMessageResourceId(int userActionNeededReason, int verificationPolicy, boolean update) {
        return 
            userActionNeededReason != DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN ?
                (userActionNeededReason != DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_NETWORK_UNAVAILABLE ?
                    /* DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_DEVELOPER_BLOCKED */
                    update ? R.string.cannot_update_app_blocked_summary : R.string.cannot_install_app_blocked_summary :
                    /* DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_NETWORK_UNAVAILABLE */
                    update ? R.string.cannot_update_verification_no_internet_summary : R.string.cannot_install_verification_no_internet_summary) :
                /* DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN */
                verificationPolicy == PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_CLOSED ?
                    update ? R.string.cannot_update_verification_unavailable_fail_closed_summary : R.string.cannot_install_verification_unavailable_fail_closed_summary :
                    /* DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_OPEN */
                    update ? R.string.cannot_update_verification_unavailable_summary : R.string.cannot_install_verification_unavailable_summary;
    }

    public final void updateUI() {
        ...
        PackageInstaller.DeveloperVerificationUserConfirmationInfo developerVerificationUserConfirmationInfo = installUserActionRequired.verificationInfo;
        int userActionNeededReason = developerVerificationUserConfirmationInfo.getUserActionNeededReason();
        int verificationPolicy = developerVerificationUserConfirmationInfo.getVerificationPolicy();
        int titleResourceId = userActionNeededReason != DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN ?
                    userActionNeededReason != DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_NETWORK_UNAVAILABLE ?
                        R.string.cannot_install_app_blocked_title : R.string.cannot_install_verification_no_internet_title :
                    R.string.cannot_install_verification_unavailable_title;
        int verificationConfirmationMessageResourceId = getVerificationConfirmationMessageResourceId(userActionNeededReason, verificationPolicy, update);
        ...
        if (verificationPolicy == PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_CLOSED &&
            userActionNeededReason == DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_NETWORK_UNAVAILABLE) {
            if (button != null) {
                button.setText(R.string.button_retry);
                button.setOnClickListener(() -> ...
                    installRepository.packageInstaller.setDeveloperVerificationUserResponse(installRepository.sessionId, PackageInstaller.DEVELOPER_VERIFICATION_USER_RESPONSE_RETRY));
            }
        } else if (userActionNeededReason == DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_UNKNOWN ||
            userActionNeededReason == DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_NETWORK_UNAVAILABLE) {
            /* PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_OPEN */
            if (verificationPolicy != PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_CLOSED) {
                if (update) {
                    this.mMoreDetailsExpandedTextView.setText(R.string.more_details_expanded_update_summary);
                    this.mInstallWithoutVerifyingTextView.setText(R.string.update_without_verifying);
                }
                ...
                mInstallWithoutVerifyingTextView.setOnClickListener(() -> ...
                    installRepository.packageInstaller.setDeveloperVerificationUserResponse(installRepository.sessionId, PackageInstaller.DEVELOPER_VERIFICATION_USER_RESPONSE_INSTALL_ANYWAY));
            }
        } else if (userActionNeededReason != DeveloperVerificationUserConfirmationInfo.DEVELOPER_VERIFICATION_USER_ACTION_NEEDED_REASON_DEVELOPER_BLOCKED) {
            Log.e("InstallationFragment", "Unknown user action needed reason: " + userActionNeededReason);
        }
        ...
    }
// res/values/strings.xml

<string name="cannot_install_app_blocked_summary">&lt;p&gt;This app was blocked. To help keep your device and data safe, only apps from verified developers can be installed.&lt;/p&gt; &lt;a href=https://g.co/android/developer&gt;Learn more about developer verification&lt;/a&gt;</string>
<string name="cannot_install_app_blocked_title">App developer unverified</string>
<string name="cannot_install_verification_no_internet_summary">&lt;p&gt;This app was not installed. Apps from unverified developers may be unsafe.&lt;/p&gt; &lt;p&gt;An internet connection is required to verify the app developer. Check your connection, then try installing again. &lt;/p&gt; &lt;a href=https://g.co/android/developer&gt;Learn more about developer verification&lt;/a&gt;</string>
<string name="cannot_install_verification_no_internet_title">No internet, can\'t verify app developer</string>
<string name="cannot_install_verification_unavailable_fail_closed_summary">&lt;p&gt;This app was blocked because there was a problem verifying the developer. To help keep your device and data safe, only apps from verified developers can be installed.&lt;/p&gt; &lt;a href=https://g.co/android/developer&gt;Learn more about developer verification&lt;/a&gt;</string>
<string name="cannot_install_verification_unavailable_summary">&lt;p&gt;There was a problem verifying this app\'s developer. Apps from unverified developers may be unsafe, so the app was not installed.&lt;/p&gt; &lt;a href=https://g.co/android/developer&gt;Learn more about developer verification&lt;/a&gt;</string>
<string name="cannot_install_verification_unavailable_title">Can\'t verify app developer</string>
<string name="cannot_update_app_blocked_summary">&lt;p&gt;This app was not updated. To help keep your device and data safe, only updates from verified developers can be installed.&lt;/p&gt; &lt;a href=https://g.co/android/developer&gt;Learn more about developer verification&lt;/a&gt;</string>
<string name="cannot_update_verification_no_internet_summary">&lt;p&gt;This app was not updated. Updates from unverified developers may be unsafe.&lt;/p&gt; &lt;p&gt;An internet connection is required to verify the app developer. Check your connection, then try updating again. &lt;/p&gt; &lt;a href=https://g.co/android/developer&gt;Learn more about developer verification&lt;/a&gt;</string>
<string name="cannot_update_verification_unavailable_fail_closed_summary">&lt;p&gt;This app was not updated because there was a problem verifying the developer. To help keep your device and data safe, only updates from verified developers can be installed.&lt;/p&gt; &lt;a href=https://g.co/android/developer&gt;Learn more about developer verification&lt;/a&gt;</string>
<string name="cannot_update_verification_unavailable_summary">&lt;p&gt;There was a problem verifying this app\'s developer. Updates from unverified developers may be unsafe, so the app was not updated.&lt;/p&gt; &lt;a href=https://g.co/android/developer&gt;Learn more about developer verification&lt;/a&gt;</string>
<string name="install_without_verifying">Install without verifying</string>
<string name="more_details_expanded_install_summary">If you install without verifying, keep in mind apps from unverified developers may put your device and data at risk.</string>
<string name="more_details_expanded_update_summary">If you update without verifying, keep in mind updates from unverified developers may put your device and data at risk.</string>
<string name="update_without_verifying">Update without verifying</string>

 

@robertkirkman
Copy link

robertkirkman commented Nov 12, 2025

One of my ideas is that if the codebases of both Termux apps are eventually combined into one, then maybe you could send both an Unsigned targetSdkVersion 28 com.termux Termux APK and an Unsigned targetSdkVersion 36 com.termux Termux APK built from the same source repository but with different gradle.properties, to the management of the com.termux Google Play account, and convince them to sign both of them with their authorized com.termux key that permits non-ADB sideloading of APKs with name com.termux, then they could upload the targetSdkVersion 36 APK to Google Play, and send you back the signed targetSdkVersion 28 APK for distribution on GitHub.

I don't think you're likely to agree to that, though, because it doesn't satisfy all of your stated requirements. The management of the com.termux Google Play account might also refuse that kind of proposal.

@agnostic-apollo
Copy link
Author

All Termux infrastructure needs to be controlled by Termux org with multiple core team members having access, no other solution is acceptable.

@robertkirkman
Copy link

robertkirkman commented Nov 12, 2025

There is a way to programmatically verify that the contents of an Unsigned APK and a Signed APK are identical other than the APK signature, so I guess what you are worried about are the other requirements that don't involve the need to verify that the distributed APK contains the same code as the APK you built in GitHub Actions, like the support contracts.

@agnostic-apollo
Copy link
Author

Issue is that control of distribution needs to belong to active core members of termux org, not one person running his own fork.

There needs to be minimal 2 people with control of every part of the infrastructure, or ideally 3 or more people, as normal redundancy in case something happens to a member. That is exactly how termux.dev domain, our server, and github/f-droid have been running for last few years and we haven't had any issues. There should never be a situation again where creator goes non responsive for 2-3 years and rest of the team having to deal with the issues and with actually no way to fix the domain or playstore app issues, and then creator coming back and then releasing his own fork without even consulting us with a higher version than ours but source code 10 versions behind, and then again for us to deal with those issues for months and even now, and with no control over it. Not to mention PlayStore income never being shared with rest of the team.

And with your solution we would still have 0 control over what is actually published on PlayStore, what would we actually be able to do if a different source code is released, like it is now?

@NoteAfterNote
Copy link

NoteAfterNote commented Nov 12, 2025

"Android Wifi transfer plugin v4.4 (2024-07-10)" is a tool by C. Ghisler that can create a WiFi Access Point (AP) or can create a WiFi Direct Server on an Android smartphone and can be used for setting up wireless debugging on another Android smartphone: https://www.totalcommander.ch/android/plugins.htm , http://totalcommander.ch/aplg/tcandroidwifi44.apk

Here are a few excerpts from https://play.google.com/store/apps/details?id=com.ghisler.tcplugins.wifitransfer (C. Ghisler, "WiFi/WLAN Plugin for Totalcmd", version 4.5 and updated on October 30, 2024): "WiFi transfer plugin and standalone app (does not require Total Commander)" "Important note: This app does NOT contain any ads. However, it contains a link to Total Commander in the upper right corner if you use a Web Browser to access the files, and this plugin as the server. This is treated as an ad by the Play Store." "Although this is mainly a plugin for Total Commander, it can also be used standalone . . ."

"New Android plugin - Wifi transfer": https://ghisler.ch/board/viewtopic.php?t=40894


NoteAfterNote-8, 'Smartphone-1 to Smartphone-2: "adb tcpip 5555" using a Linux server, android-tools, Termux, termux-usb, usbredirect, and QEMU': https://gist.github.com/NoteAfterNote/ee883d5fd86c3b8ef0b0b84cac47b4d6 from https://gist.github.com/NoteAfterNote or https://github.com/NoteAfterNote , https://web.archive.org/web/20240613213811/gist.github.com/NoteAfterNote/ee883d5fd86c3b8ef0b0b84cac47b4d6

@agnostic-apollo
Copy link
Author

Whether the secondary device is a PC or mobile, you still need to connect it via a USB. And like I said, running multiple shell commands or a million in your case is not in the capabilities of a normal user. And if you want to use F-Droid/GitHub Termux to run these commands and APKs are unverified, then how will you install them beforehand in the first place to run the adb commands?

Stop suggesting any variant of adb for normal users, its meant for developers, not normal users and normal apps.

@robertkirkman
Copy link

robertkirkman commented Nov 13, 2025

Apart from that being a terrible install method for non-developer users that not only will affect Termux but other apps, not everyone is privileged enough to have a PC to actually be able to run USB debugging. How many people do you think are staying on older Android devices by choice instead of not having the money to buy a newer phone? And if they can't buy a new phone, what are the chances they own a PC/laptop or have a regular access to it? And again, this is not just about Termux, but normal apps too.

Stop suggesting any variant of adb for normal users, its meant for developers, not normal users and normal apps.

I disagree that ADB is not for normal users. Many people who are not developers already use it, and, speaking generally about Android apps and not specifically about Termux, I believe that if the Google policy change really does cause adb install to become the only fully working method of sideloading, then many more people will begin to use adb install than currently do.

Speaking about Android apps generally, scrcpy is a widely popular remote desktop software for Android that is used by many normal users who are not developers, but has always required ADB from the beginning, so has always required users to understand ADB in order to use it, so at least all scrcpy users will already understand ADB.

If adb install does become the only fully working method of sideloading, then I fully expect other, more generalized tools to continue to appear at an increased rate to continue to make ADB more accessible to general users. Someone might even make a tiny USB dongle for Android phones' USB ports out of an Arduino or Rpi Zero or something and then sell it to help people auto-connect to ADB without a separate PC.

@marcprux
Copy link

Thanks for the comprehensive write-up, @agnostic-apollo. Would you mind if I add a link to this from the reference section of keepandroidopen.github.io?

@agnostic-apollo
Copy link
Author

@marcprux You are welcome, and feel free to refer this or any of my other public links anywhere, no one needs my explicit permission. Thanks to you as well for making a collection.

@marcprux
Copy link

@agnostic-apollo
Copy link
Author

Good news! Google has announced in their blog at https://android-developers.googleblog.com/2025/11/android-developer-verification-early.html that:

Based on this feedback and our ongoing conversations with the community, we are building a new advanced flow that allows experienced users to accept the risks of installing software that isn't verified.

@marcprux
Copy link

And FTR:

I wonder if F-Droid was directly contacted...

We were not contacted prior to the announcement.

@agnostic-apollo
Copy link
Author

@marcprux Thanks for the add.

Ah, I see. I assume you mean the initial announcement of introducing developer verification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment