Back to Research
AndroidAPK ModdingReverse Engineering

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:authorities value another installed app already registers gives INSTALL_FAILED_CONFLICTING_PROVIDER.
  • A <permission android:name> another app already declares gives INSTALL_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.

AndroidManifest.xml

Renamed — 9 authorities, 7 permissions

Everything still carrying a com.coloros.* or com.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_oneplus ships as required="true" and the Find X9 Pro does not declare that feature, so it becomes required="false". Confirm the device's side of it with adb shell pm list features.

Left alone — the queries block

<queries> stays. Its com.oplus.statistics.provider entry declares an intent to read another app's provider, not one this app registers, so nothing about it conflicts with anything.

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.

AuthorityOccurrencesForm
com.coloros.alarmclock.alarmclock3110 bare, 21 inside content:// URIs
com.oplus.alarmclock.alarmclock4bare and URI
com.oplus.alarmclock.sphelper2bare
com.oplus.alarmclock.dial_clock_cities1URI
com.oplus.alarmclock.provider.communicate1bare

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.data
  • com.oplus.weather.service.provider.data
  • com.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:

PackageRequired certificate SHA-1
com.oneplus.deskclockB7: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 ordercom.coloros.alarmclockstock, platform-signedcom.oneplus.deskclockported, re-signed
Privacy terms acceptedA precondition, settled by the device rather than by who is asking.passpass
Certificate whitelistDoor one. A package name paired with one exact signing certificate.passfailOn the list by name, wrong fingerprint
Signature permissionDoor two. Signature-level against the platform, so only platform-signed apps hold it.passfailNot a grantable permission type
Widget reads13°--°
Passing either door is enough, so the stock clock never has to rely on one of them. A refused caller is handed an empty cursor rather than a permission error, which is why the ported widgets read as missing data rather than as a rejection — the refusal itself is only visible in logcat, under 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:

VectorResult
All 28 weather provider authoritiesNone readable — 3 gated, 25 erroring
settings global, system, secureNo cached reading, only the key_com_oplus_weather_service flag
WEATHER_INFO_CHANGED broadcastA re-query trigger carrying no payload; the widget reads the provider, not the intent extras
Weather app notificationsOccasional TravelReminder alerts, no standing temperature
A third-party weather app to read insteadNone 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.