Managing Android API 33+ Permissions in Appcelerator Titanium
Handle Android 13+ runtime permissions in Appcelerator Titanium by adding native permission overrides and verifying the generated build.
08 Jul 2025, 09:04 UTC

The problem: native APIs that stop working after an Android upgrade
You maintain an Appcelerator Titanium mobile app that works on Android 12 and earlier. After you raise the target SDK to API 33 (Android 13) or higher, calls to the camera, location, or nearby device APIs stop returning results. The JavaScript does not throw an obvious error; the native permission dialog simply never appears.
Titanium compiles JavaScript to native modules and exposes a single cross-platform API for features such as camera, geolocation, and accelerometer. That abstraction worked well when Android permissions were mostly granted at install time. Starting with API 33, Android added runtime permission groups and stricter rules for Bluetooth, Wi-Fi, and notifications. Titanium's abstraction does not cover every new permission, so the lightweight bridge between JavaScript and native views can fail silently.
Check the SDK version before changing code
Open a terminal in the Titanium project root and run:
ti info
This command requires no special permissions. Look for the Titanium SDK version in the output. If your project uses Titanium SDK 6.x or 7.x, note that 7.5.1.GA (2020) is the last major release, so older projects may lack API 33+ support and have outdated community modules.
Next, build a debug Android package so you can inspect what Titanium actually generates:
ti build -p android -b debug
Run this in the project root. After the build finishes, open build/android/AndroidManifest.xml and build/android/app/build.gradle (paths vary by Titanium version). Confirm that compileSdkVersion and targetSdkVersion are 33 or higher, and that the manifest declares the permissions you expect. Do not edit the generated files directly; Titanium can overwrite them on the next build. Make changes in tiapp.xml instead.
Worked example: declare and request camera access on API 33+
Add the manifest permissions in tiapp.xml inside the <android> section:
<android xmlns:ti="http://ti.appcelerator.org">
<manifest>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
</manifest>
</android>
Declaring a permission is not the same as granting it at runtime. A runtime permission is one the user approves while the app is running, rather than during installation. On API 33+, your JavaScript should check the platform and API level before opening the camera:
var apiLevel = parseInt(Ti.Platform.apiLevel, 10);
if (Ti.Platform.osname === 'android' && apiLevel >= 33) {
if (Ti.Android.hasPermission && Ti.Android.hasPermission(Ti.Android.PERMISSION_CAMERA)) {
openCamera();
} else {
Ti.Android.requestPermissions([Ti.Android.PERMISSION_CAMERA], function(e) {
if (e.success) {
openCamera();
} else {
Ti.UI.createAlertDialog({title: 'Permission needed', message: 'Camera access is required.'}).show();
}
});
}
} else {
openCamera();
}
Replace openCamera() with your existing camera logic. Ti.Platform.apiLevel is the Android API level reported by the device, not the SDK version you compile against. If Ti.Android.requestPermissions or the permission constant is missing in your SDK version, that is a signal to write a small native Android module or override rather than forcing the JavaScript bridge.
After saving tiapp.xml, rebuild with ti build -p android -b debug and inspect the generated AndroidManifest.xml again. The camera permission line should appear in the manifest, and a test build on an API 33 emulator or device should show the system permission dialog when openCamera() runs.
Trade-offs and limitations
- Maintenance surface: Native overrides break the write-once, run-anywhere promise. You now maintain Android-specific code alongside shared JavaScript.
- SDK maturity: Titanium SDK 7.5.1.GA (2020) is the last major release. Community modules and documentation may be outdated, and API 33+ gaps may require custom native code.
- Backend lock-in: If the app uses Appcelerator Cloud Services, push notifications and backend handling differ across SDK 6.x and 7.x. Long-term projects should evaluate migration to an external backend-as-a-service rather than relying on proprietary cloud APIs.
What to do next
- Run
ti infoand record the Titanium SDK version. - Set
targetSdkVersionto 33 or higher intiapp.xmland rebuild a debug APK. - Inspect
build/android/AndroidManifest.xmlto confirm permissions are declared. - Test camera, location, and Bluetooth flows on an API 33+ device or emulator.
- Where the Titanium bridge is missing a permission constant, add a native Android override and document it for the team.
If your team is still using Appcelerator Cloud Services for push or data, treat that as a separate migration risk. Fixing the permission bridge keeps the app running today; moving off proprietary backend APIs keeps it maintainable tomorrow.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.