Android 14 Adaptation Guide: Practical Solutions for Upgrading targetSdkVersion to 34

1. Preface

With the release of Android 14, as an Android developer, we need to adapt our existing applications to ensure their compatibility and performance on the new system. This article will detail the changes to be aware of when upgrading the targetSdkVersion to 34, as well as the adaptation strategies. Additionally, you can use the latest AI to generate the most recent code.

2. Core Functionality Changes

2.1. Foreground Service Types

Android 14 mandates specifying service types for foreground services. This means that if your app uses foreground services, you need to add the android:foregroundServiceType attribute to each <service> tag in your AndroidManifest.xml.

For example, if your app has a music playback service, you would declare it like this:

<service
    android:name=".MusicPlayerService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="false">
</service>

Also, ensure your service calls the startForeground() method when starting, passing a valid notification.

Foreground service types were introduced in Android 10. By specifying android:foregroundServiceType, you can define the service type for <service>. The available foreground service types include:

  • mediaPlayback: For continuous background audio or video playback, or DVR functionality on Android TV.
  • mediaProjection: For projecting content to non-primary displays or external devices using the MediaProjection API. This content is not necessarily media-specific.
  • microphone: For continuous background microphone capture, such as in a recorder or communication app.
  • phoneCall: For ongoing use of the ConnectionService API.
  • camera: For continued access to the camera in the background, such as in a multitasking video chat app.
  • connectedDevice: For interaction with external devices requiring Bluetooth, NFC, IR, USB, or network connections.
  • dataSync: For data transfer operations, such as data upload or download, backup and restore, import or export, data retrieval, local file processing, and data transfer between devices and the cloud. (This type may be deprecated in future Android versions; consider using WorkManager or user-initiated data transfer jobs instead.)
  • health: For long-running use cases supporting fitness apps, such as activity trackers.
  • location: For long-running use cases requiring location access, such as navigation and location sharing.
  • remoteMessaging: For transferring text messages from one device to another. Helps ensure continuity of user messaging tasks when switching devices.
  • shortService: For important work that needs to be completed quickly and cannot be interrupted or postponed; has 5 characteristics: 1) Can only run for a short duration, about 3 minutes; 2) Does not support sticky foreground services; 3) Cannot start other foreground services; 4) Does not require specific permissions other than FOREGROUND_SERVICE; 5) Running foreground services cannot switch between shortService types. After timeout, Service.onTimeout() will be called, which is a new API in Android 14, and it is recommended to implement the onTimeout callback to avoid ANR.
  • specialUse: If none of the above types apply, use this type. In addition to declaring the FOREGROUND_SERVICE_TYPE_SPECIAL_USE foreground service type, you should also declare the use case in the Manifest by specifying a <property> element within the <service> element, as shown below. These values and corresponding use cases will be reviewed when submitting the app to Google Play Console.
<service android:name="fooService" android:foregroundServiceType="specialUse">
  <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="foo"/>
</service>
  • systemExempted: Reserved for system apps and specific system integrations to continue using foreground services. Regular app developers need not worry about this.
  • Additionally, among the 13 types listed above, those marked in color are new in Android 14; the others were already available.

2.2. OpenJDK 17 Updates

Android 14 has updated compatibility with OpenJDK 17. This may affect apps that use regular expressions and the UUID.fromString() method. Ensure your app has no issues in these areas, or test by turning off compatibility mode in developer options. Some of the changes that may affect app compatibility include:

  • Regular expression changes: Some regular expressions have changed, so check where regular expressions are used in your app to see if there are any errors. You can turn off compatibility mode in developer options to help identify problematic areas. Specifically, go to System > Advanced > Developer Options > App Compatibility Changes (for native systems, other manufacturers may vary), and select your app from the list to toggle the DISALLOW_INVALID_GROUP_REFERENCE option.
  • UUID handling: The java.util.UUID.fromString() method will perform stricter checks when validating input parameters, which may result in an IllegalArgumentException being thrown during deserialization. The self-test method is the same as above; you need to toggle the ENABLE_STRICT_VALIDATION option under App Compatibility Changes.
  • ProGuard issues: In some cases, using ProGuard to compress, obfuscate, and optimize code can cause problems after adding java.lang.ClassValue. This issue arises from a change in runtime behavior by a Kotlin library, specifically whether executing Class.forName("java.lang.ClassValue") will return a class, which can cause problems if the app was developed for older versions without java.lang.ClassValue. These optimizations may remove the computeValue method from classes derived from java.lang.ClassValue.

2.3. Bluetooth Connection Permission Changes

On Android 14, using the BluetoothAdapter's getProfileConnectionState() API requires the BLUETOOTH_CONNECT permission. You need to declare this permission in your AndroidManifest.xml and request user authorization at runtime.

3. Security Enhancements

3.1. Restrictions on Implicit Intents and PendingIntents

Implicit Intents are a mechanism for communication between Android application components that do not explicitly specify which component to start but declare the action to be performed. The system finds components that can handle the action and starts them. Implicit Intents are mainly used to trigger various actions within an app or between other apps, such as starting activities, services, broadcasting, etc. A common example is setting the action in the intent-filter of an Activity in the Manifest file, then matching and starting that Activity by setting the action in the launch Intent.

Implicit Intents can only be sent to components with android:exported="true" (the four major components: Activity, Service, etc.). So when using Intents to pass data in an app, either use explicit Intents for components with android:exported="false", or use implicit Intents for components with android:exported="true". Of course, explicit Intents can also be sent to exported="true" components.

A mutable PendingIntent must set the packageName, or an exception will be thrown.

Android 14 introduces new restrictions on Implicit Intents and PendingIntents. Ensure your app does not crash due to these restrictions. For Implicit Intents, they can only be sent to components with android:exported="true". For PendingIntents, mutable PendingIntents must set the packageName.

3.2. Export Behavior of Dynamically Registered Broadcast Receivers

Dynamically registered broadcast receivers must specify whether they are exported. Register using the ContextCompat.RECEIVER_EXPORTED or ContextCompat.RECEIVER_NOT_EXPORTED flags. This feature was introduced in Android 13, allowing apps to specify whether a registered broadcast receiver should be exported and visible to other apps on the device. On Android 14, it has become "mandatory." In previous Android versions, any app on the device could send unprotected broadcasts to dynamically registered broadcast receivers unless the receiver had a signature permission.

3.3. Safer Dynamic Code Loading

All dynamically loaded files must be marked as read-only. Before writing file content, ensure the file is set to read-only. The official recommendation is for apps to avoid dynamically loading code as much as possible, as this significantly increases the risk of the app being compromised by code injection or tampering.

If dynamic code loading is necessary, dynamically loaded files (such as DEX, JAR, or APK files) must be set to read-only before the file is opened and any content is written:

val jar = File("DYNAMICALLY_LOADED_FILE.jar")
val os = FileOutputStream(jar)
os.use {
    jar.setReadOnly()
}
val cl = PathClassLoader(jar.absolutePath, parentClassLoader)

Additionally, to prevent the system from throwing exceptions on existing dynamically loaded files, the official recommendation is to delete and recreate the files before attempting to dynamically reload them in the app. When recreating files, mark them as read-only when writing, as per the guidelines above. Alternatively, existing files can be re-marked as read-only, but in this case, it is recommended to first verify the file's integrity (e.g., check the file's signature against a trusted value) to protect the app from malicious manipulation.

3.4. Zip Path Traversal

Android 14 prevents Zip path traversal vulnerabilities by throwing a ZipException. Ensure your app is not affected by this change. If you do not want to throw an exception and cannot change the file names, you can opt-out of validation by calling dalvik.system.ZipPathValidator.clearCallback(). However, this is not recommended. Zip path traversal vulnerability refers to a security risk where a malicious attacker can access arbitrary files or directories outside the Zip file by constructing file paths with "../" or starting with "/", during the decompression of Zip files, thereby posing a security risk to the application.

3.5. New Restrictions on Starting Activities from the Background

Android 14 further restricts the ability to start Activities from the background. Ensure your app complies with the new rules when using PendingIntent or bindService().

  • When an app uses PendingIntent#send() or similar methods to send a PendingIntent, it must choose whether to grant itself permission to start background Activities to send the PendingIntent. If granting permission, it needs to return an ActivityOptions object by calling the setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED) method.
  • When a foreground-visible app uses bindService() to bind a Service from a background app, the visible app must now choose whether to grant its background Activity start permission to the bound service. If granting permission, the app should set the BIND_ALLOW_ACTIVITY_STARTS flag when calling bindService().

These changes extend the existing set of restrictions by protecting users from malicious apps that abuse APIs to launch disruptive Activities from the background.

4. Restrictions on Non-SDK Interfaces

Generally, public SDK interfaces are those documented in the Android framework package index. Android 14 updates the list of restricted non-SDK interfaces. If your app uses non-SDK interfaces, you need to plan to migrate to SDK alternatives or request new public APIs from the official sources.

Adaptation suggestions:

  • Test existing functionality: Thoroughly test your app on Android 14 devices or emulators. If you're unsure whether your app uses non-SDK interfaces, you can run the test app in Debug mode. If the app accesses certain non-SDK interfaces, the system will output a log message. Check the app's log messages for the following details:
  1. The declared class, name, and type (in the format used by the Android runtime);
  2. The access method: linking, reflection, or JNI;
  3. Which list the accessed non-SDK interface belongs to;

You can also use adb logcat to view these log messages, which appear under the PID of the running app. For example, the log might contain an entry like:

Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
  • Update dependencies: Ensure all third-party libraries used by your app are up-to-date and compatible with Android 14.
  • User experience: Considering the new permission requests and system behavior changes, ensure your app provides a good user experience, especially when requesting permissions and handling background tasks.
  • Pre-release testing: Before upgrading targetSdkVersion, release the app through Alpha or Beta channels to collect feedback and resolve any issues that may arise.