Porting the OnePlus Clock to ColorOScom.oneplus.deskclock on a Find X9 Pro
Both apps are OPPO-family builds of one codebase, so the collision is not the package name — it is every authority, permission and content URI underneath it. Renaming those is mechanical. What ends the port is a check the APK has no say in: the weather service reads the calling app's signing certificate, and a rebuild does not have one it will accept.
Authorities the stock clock held
11 of 15
Read out of dumpsys rather than guessed, which is the only way to know which of them actually collide. The other four were free.
Renamed in smali
16 files
Thirty-nine authority strings, plus two permissions the code also spells out. The manifest is half the job.
Widgets that work
1 of 9
DigitalAppWidgetProvider. The other eight are weather widgets, and all eight stay blank.
Weather would have worked on
ColorOS 14
Below the version gate the service checks the package name alone. This is something OPPO tightened, not a long-standing rule.
Table of contents
Base app: com.oneplus.deskclock from OxygenOS 16.19.4. Target: a Find X9 Pro
(CPH2791) on ColorOS 16, which reports ro.build.version.oplusrom V16.1.0. The
stock clock it installs beside is com.coloros.alarmclock, at
/product/app/Clock/Clock.apk. The port was removed again once the weather
limit was established — everything below is a record of what it took and where
it stopped.
What was ported
The OnePlus 15 clock installs and runs beside Oppo's stock clock on ColorOS once every internal identity string it shares with that app is renamed. Both are OPPO-family builds of one codebase, so the package names already differ and the whole collision sits in the strings underneath.
Worth being precise about which three names are in play, because only two of
them exist on the device. com.coloros.alarmclock is the stock clock and it is
installed. com.oneplus.deskclock is the port. com.oplus.alarmclock is not
installed at all — it is only what the OnePlus APK's <original-package> tag
names, and that tag turns out to matter.
The app runs, and everything it works out for itself comes out right. What never works is weather, and no edit to the APK fixes it, because the decision is not made inside the APK at all.
Why the package name stays
The package name stays com.oneplus.deskclock, which is the opposite of the
usual move. Renaming the package is how a modified system app gets installed
without root — it is what makes Android register a distinct app rather than a
conflicting signature for one it already trusts, and it is what
the GBoard mod does.
Here it breaks the app. This one branches on its own package name in many
places, and those branches are what select the OnePlus behaviour. i7/y->T() is
just getPackageName().equalsIgnoreCase("com.oneplus.deskclock"), and
SPContentProvider calls it to choose between two authority constants. Renaming
the package silently flips every one of those branches to the Oppo side, so the
app installs cleanly and then quietly takes the Oppo path everywhere it was
built to take the OnePlus one.
So the package name is the one identity string that does not change, and everything underneath it changes instead.
What blocks a second install
Two manifest conflicts, each fatal on its own:
- An
android:authoritiesvalue another installed app already registers givesINSTALL_FAILED_CONFLICTING_PROVIDER. - A
<permission android:name>another app already declares givesINSTALL_FAILED_DUPLICATE_PERMISSION.
<original-package android:name="com.oplus.alarmclock"/> is a third problem of
a different kind. It is the AOSP rename-inheritance tag, and it makes Android
treat the install as an upgrade of that package rather than as a new app. Delete
the line.
Which authorities actually conflict is a question with an answer, so it is worth
asking the device rather than assuming the two apps overlap completely.
dumpsys package providers prints authorities in bracket form:
adb shell dumpsys package providers | grep -A1 "^ \[com.oplus.alarmclock.alarmclock\]:"
Eleven of the fifteen authorities in the OnePlus manifest were genuinely held by
com.coloros.alarmclock. The other four were free.
The manifest changes
Appending .op to every authority and permission not already namespaced under
com.oneplus.deskclock clears both conflicts at once.
- Renamed — 9 authorities, 7 permissions
Everything still carrying a
com.coloros.*orcom.oplus.*name gets the suffix. Names already under the app's own package —com.oneplus.deskclock.epona,.tingle,.androidx-startup,.FileShareProvider— collide with nothing and stay exactly as they are.- Deleted — 9 provider blocks
Deleted rather than renamed, because they serve card frameworks the port has no use for: four Seedling cards, the quick card, the settings search indexer, two AI support providers, and data backup. A provider that is never going to be read is not worth a name.
- Relaxed — one required feature
oplus.software.support_gp.brand_oneplusships asrequired="true"and the Find X9 Pro does not declare that feature, so it becomesrequired="false". Confirm the device's side of it withadb shell pm list features.- Left alone — the queries block
<queries>stays. Itscom.oplus.statistics.providerentry declares an intent to read another app's provider, not one this app registers, so nothing about it conflicts with anything.
AndroidManifest.xml
The same names, in smali
The manifest is half the job. An authority the code builds a content:// URI
from has to match the manifest too, or the app cannot find its own provider.
Sixteen files needed the suffix.
| Authority | Occurrences | Form |
|---|---|---|
com.coloros.alarmclock.alarmclock | 31 | 10 bare, 21 inside content:// URIs |
com.oplus.alarmclock.alarmclock | 4 | bare and URI |
com.oplus.alarmclock.sphelper | 2 | bare |
com.oplus.alarmclock.dial_clock_cities | 1 | URI |
com.oplus.alarmclock.provider.communicate | 1 | bare |
Two permission strings are live in code and need the same treatment:
com.oplus.alarmclock.permission.DATA_BACKUP_SAFE, and the com.coloros
spelling of ACCESS_CLOCK_RECEIVER_PLATFORM.
Four of the renamed authorities appear nowhere in the smali at all —
com.oplus.alarm.provider.widget.wplus,
com.coloros.alarmclock.provider.dialclock,
com.oplus.alarmclock.provider.dialworldclock and
com.oplus.alarmclock.provider.timercard. The framework resolves those from the
manifest, so renaming them there is the whole of the work.
A blind replace breaks the app
com.oplus.alarmclock.alarmclock is also a prefix of roughly 25 broadcast
action names — .enter_apk, .cancel_snooze, .clear_notification — and of
class-name strings. Suffixing those renames actions the rest of the system still
sends under the old name.
Append the suffix only where the next character is " or /. That catches bare
constants and URI authorities, and skips everything dotted:
re.sub(re.escape(auth) + r'(?=["/])', auth + ".op", text)
Keep .bak copies outside the decoded tree. grep -r reads them too, and
reports the unpatched originals as though the patch had failed — which looks
exactly like a rename that did not take.
Weather never works
Eight of the nine appwidget receivers in the OnePlus manifest are
com.coloros.widget.smallweather.* weather widgets. Only
DigitalAppWidgetProvider is clock-only, and it is the one that fully works.
The reason is in a different app. com.coloros.weather.service releases weather
data only to callers on a hardcoded list of package names paired with signing
certificate SHA-1 fingerprints. The list is built in
com/oplus/weather/service/utils/d.smali and enforced from
WeatherProvider.smali; OPWeatherProvider and OPlusWeatherProvider both
extend WeatherProvider, so all three data authorities are gated identically:
com.coloros.weather.service.provider.datacom.oplus.weather.service.provider.datacom.oplusos.weather.service.provider.data
All three are exported="true" and carry no manifest permission, so the gate is
invisible until you read the code:
if (!privacyAccepted) return false
if (d.c(context, callerPackage)) return true // certificate whitelist
if (checkCallingOrSelfPermission(OPPO_COMPONENT_SAFE) == GRANTED) return true
return false
The whitelist demands one exact fingerprint per package:
| Package | Required certificate SHA-1 |
|---|---|
com.oneplus.deskclock | B7:91:45:D7:9F:8F:14:C2:6C:68:EC:BB:27:8D:56:AE:43:65:B1:61 |
A debug-signed rebuild presents something else. The stock
~/.android/debug.keystore gives
DF:E2:EE:2A:7D:8F:11:5C:AE:C9:8D:25:70:6C:3E:31:F6:34:7B:77, and is refused.
The second door does not open either. oppo.permission.OPPO_COMPONENT_SAFE has
a protectionLevel of signature against the oplus platform package, so it
cannot be granted to a third-party app, and pm grant refuses in as many words:
SecurityException: Permission oppo.permission.OPPO_COMPONENT_SAFE requested by
package com.oneplus.deskclock is not a changeable permission type
Shizuku does not help. That refusal came from an adb shell, which is the exact privilege level Shizuku hands out.
One gate, two callers
| Checked in order | com.coloros.alarmclockstock, platform-signed | com.oneplus.deskclockported, re-signed |
|---|---|---|
| Privacy terms acceptedA precondition, settled by the device rather than by who is asking. | pass | pass |
| Certificate whitelistDoor one. A package name paired with one exact signing certificate. | pass | failOn the list by name, wrong fingerprint |
| Signature permissionDoor two. Signature-level against the platform, so only platform-signed apps hold it. | pass | failNot a grantable permission type |
| Widget reads | 13° | --° |
WeatherS_ApkSignedCheck.Both outcomes are logged under WeatherS_ApkSignedCheck, so the decision is
observable rather than inferred. With the stock clock's weather widget and a
ported one on the same home screen, the service judges the pair in the same
breath:
checkPackageSignature: signature mismatch, packageName=com.oneplus.deskclock, actualSignature=DF:E2:EE:2A:7D:8F:11:5C:AE:C9:8D:25:70:6C:3E:31:F6:34:7B:77
checkCertificateWhiteList: has safe componentcom.coloros.alarmclock
Stock com.coloros.alarmclock passes both doors at once by being
platform-signed. The whitelist does carry an entry for
com.oneplus.deskclock, so the port is a caller the service expects — a rebuild
keeps the name and loses the fingerprint, and that is the whole of the problem.
Everything the app computes for itself is correct: time, day, date, layout, fonts, even the weather icon slot. The single element that needs another app's permission is the single element that fails.
Nothing to piggyback off
The gate guards more than a temperature. attent_city and resident_city sit
in the same provider as the reading, so location history is behind the same
door, and the readings themselves come from a paid upstream feed.
Borrowing another app's access is not available in principle. A binder call carries the caller's own UID, the service resolves that to a package and a signature, and Android offers no way to call under another app's identity. That leaves only a whitelisted app willing to re-export what it reads, and on a Find X9 Pro none exists. Every candidate was queried:
| Vector | Result |
|---|---|
| All 28 weather provider authorities | None readable — 3 gated, 25 erroring |
settings global, system, secure | No cached reading, only the key_com_oplus_weather_service flag |
WEATHER_INFO_CHANGED broadcast | A re-query trigger carrying no payload; the widget reads the provider, not the intent extras |
| Weather app notifications | Occasional TravelReminder alerts, no standing temperature |
| A third-party weather app to read instead | None installed |
Shell hits the same wall. content query against any of the three authorities
returns No result found. while the weather app itself displays a current
reading, because shell is not on the list either. An empty result there is the
whitelist refusing, not an empty database.
There is a second, independent blocker in the other direction. Even a port that
could read the provider would not be told when to.
SystemProp.isOnePlusExp() compares Build.BRAND against ONEPLUS, and the
weather service uses the result to choose which clock package to notify. A Find
X9 Pro reports a different brand, so weather-updated broadcasts go to
com.coloros.alarmclock and never reach a ported OnePlus clock.
What is left is abandoning the OPPO service and fetching from a public API. The
clock already holds INTERNET and Open-Meteo needs no key, but the work lands
in obfuscated smali in the widget's own data path, which is a different and much
larger job than renaming strings.
The signature check is new
d.c() consults SystemProp.isAboveOS14(), which compares the OS version
against 30. Below that line it returns on the package name alone and never looks
at the certificate. At or above it, the name only gets a caller as far as the
signature comparison. ColorOS 16 reports ro.build.version.oplusrom V16.1.0 and
is over the line.
The same port onto ColorOS 14 or earlier would therefore have had working
weather: a re-signed APK keeping the name com.oneplus.deskclock would have
passed on the name. This is a gate OPPO tightened, not one that was always
there.
Building it, and removing it
The usual apktool round trip applies — decompile, edit, rebuild, zipalign, sign
— the same process the GBoard mod documents, with one addition worth
making a habit. Read the compiled manifest back with aapt2 afterwards and
confirm the renames actually survived the rebuild, rather than trusting the
decoded XML you edited:
aapt2 dump xmltree --file AndroidManifest.xml Clock-op.apk | grep -oE 'authorities.*="[^"]*"'
Both apps then carry the label Clock, so the launcher and the widget picker
show two identical entries with no way to tell them apart until one is opened.
Removing it again is a plain uninstall, the stock clock having never been touched at any point:
adb uninstall com.oneplus.deskclock
Which is how this ended. The technique works and the app runs; the weather limit is what made it not worth keeping over the stock clock.