Stuff That Might Interest You

Then, we have some items that will not break your app but represent features that you might want to opt into, for Android 10 devices.

Preference Deprecation

The framework set of Preference classes are marked as deprecated on Android 10.

This makes some sense. Preference and its subclasses (e.g., EditTextPreference, ListPreference) are wrappers around widgets. However, those wrappers wrap framework widgets and therefore may not adapt to AppCompat themes.

Google steers you in the direction of the AndroidX preference library (androidx.preference:preference and androidx.preference:preference-ktx). This appears to be their direction, at least for the next few years.

Note, though, that the AndroidX preference library does not have a RingtonePreference, nor does Google plan to add one. On the other hand, the AndroidX preference library does offer DropDownPreference and SeekBarPreference, which were lacking in the native preference system.

ACTION_SEND Previews

Android 10 offers a new content preview feature when using ACTION_SEND, where the user can see a customized preview of what it is that they are sharing.

To implement this, you can:

On the plus side, this requires no new APIs, so this code will work on older versions of Android.

However, this feature has issues:

Settings Panels

We have long had a series of Intent actions defined on the Settings class to be able to launch into a particular screen of the Settings app, using startActivity().

Android 10 extends this with “panel” actions. These launch a screen in the Settings app that is styled as a bottom-sheet dialog:

Internet Connectivity Settings Panel
Internet Connectivity Settings Panel

There are four such panel actions:

Changes in Device Authentication

If you have been using KeyguardManager and its createConfirmDeviceCredentialIntent() method to authenticate the user, it has been deprecated on Android 10. Like most deprecations, this method still works. However, Google is steering you in a different direction: using setDeviceCredentialAllowed() with BiometricPrompt.

A Quick Device Authentication Recap

Sometimes, we want to confirm that the person holding the device is the authorized user of that device, before proceeding with something sensitive.

For devices with a minSdkVersion of 21 or higher, the simple solution was to use createConfirmDeviceCredentialIntent() on KeyguardManager. This would return an Intent that you could pass to startActivityForResult(). It would bring up the system PIN/password screen to authenticate the user, letting you know of success or failure via onActivityResult().

However, that approach only offers the PIN/password option. It does not allow the user to use biometrics, such as a fingerprint, to authenticate. For that, we have BiometricPrompt on Android 9.0+ (or FingerprintManager and FingerprintDialog for API Level 23-27). Those allow the user to authenticate using biometrics… but only via biometrics. That does not work for users who have not set up a biometric authentication option, or for devices that lack biometric capability.

Compounding the complexity is Android 10’s deprecation of createConfirmDeviceCredentialIntent().

setDeviceCredentialAllowed()

The replacement is setDeviceCredentialAllowed() on BiometricPrompt. On Android 10 devices, this brings up the PIN/password screen if either:

Presumably, it will also use the PIN/password screen for devices that lack any biometric hardware, though this has not been tested.

The SecureCheq sample module in the book’s sample project demonstrates the use of setDeviceCredentialAllowed(). This sample app is based on a sample that originally appeared in The Busy Coder’s Guide to Android Development for showing how to use BiometricPrompt. Here, it is converted to Kotlin and tweaked to employ setDeviceCredentialAllowed(), along with another new Android 10 method, setConfirmationRequired().

As with that earlier sample, here use a BiometricPrompt.Builder to create an instance of BiometricPrompt:

    val prompt = BiometricPrompt.Builder(this)
      .setTitle("This is the title")
      .setDescription("This is the description")
      .setSubtitle("This is the subtitle")
      .setConfirmationRequired(true)
      .setDeviceCredentialAllowed(true)
      .build()

setConfirmationRequired() is primarily for non-fingerprint sorts of biometrics, such as face recognition. Those can be fairly automatic: if the user happens to be looking at the screen at the time of authentication, no actual user input is required. This may be too easy, and it is why Android allows the user to disable “passive” authentication. You too can disable passive authentication, via setConfirmationRequired(true). This indicates that for biometrics like face recognition, that you want the user both to authenticate and tap an on-screen button to confirm that they wish to proceed. This is new to Android 10.

Also new to Android 10 is setDeviceCredentialAllowed(). As noted, this will fall back to PIN/password authentication, if biometrics are unavailable or opted-out by the user, though there appears to be a bug here. In the sample code, we call setDeviceCredentialAllowed(true) to request this behavior. If you do not call setDeviceCredentialAllowed(true) — either by passing false or skipping the call entirely — you must call setNegativeButton(), to provide details of what to do if the user elects to skip the authentication process entirely. We will see an example of that shortly.

Nothing changes with our authenticate() call or the AuthenticationCallback object. However, there should be a slight behavior change with the onAuthenticationError() function on the AuthenticationCallback. onAuthenticationError() can be called with an error code of BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS to indicate that the user has not enrolled any fingerprints or other biometric identifiers. In the case of setAllowDeviceCredential(true), though, this should not occur. Instead, if the user has no registered identifiers, the user will be sent to the PIN/password screen to authenticate that way.

Note that the androidx.biometric library offers a backwards-compatible implementation of BiometricPrompt that includes an implementation of setDeviceCredentialAllowed(). This does not give prior versions of Android this capability, but it will “gracefully degrade” on older devices.

Learning More About MIME Types

ContentResolver offers a getTypeInfo() method. Given a MIME type, it returns a MimeTypeInfo object. This offers:

The TypeInfo sample module contains a small app that presents a list of 65 MIME types and the associated label and icon from Android 10’s MimeTypeInfo:

TypeInfo Sample App, Showing Some MIME Types
TypeInfo Sample App, Showing Some MIME Types

Given a long listOf() MIME types named MIME_TYPES, MainMotor loads them in the background using a ContentResolver and maps the MimeTypeInfo data into a RowState for use by the UI:

  private suspend fun mapTypes(context: Context) =
    withContext(Dispatchers.Default) {
      val resolver = context.contentResolver

      MIME_TYPES
        .map { RowState(it, resolver.getTypeInfo(it)) }
        .sortedBy { it.description.toString() }
    }

This is useful for cases where you have an arbitrary Uri and you want to have more information about it for presentation, whether in a list like TypeInfo or for single-Uri “attachments”. Given a Uri and DocumentFile.fromSingleUri(), you can get the MIME type and display name; MimeTypeInfo just gives you more information about the type.

However, the data returned in a MimeTypeInfo object is very generic in general. Mostly, they seem to have a category of types, which controls the icon and part of the label (e.g., “Archive”, “Audio”). In many cases, if the MIME type is not recognized, the label is simply “File”. For casual use, this may be acceptable, but serious apps should consider serious solutions (and ones that are not tied to Android 10).

Foreground Service Types

Depending on what your app does, you may start seeing crashes with the following sort of error message:

Caused by: java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION

Android 10 adds an android:foregroundServiceType attribute to the <service> element in the manifest. We saw this used for a location value in the chapter on location changes. While this is not documented on the <service> element itself, there is a bit of documentation in the R.attr JavaDocs.

Depending on what your app does in its service, you may have a total of three requirements now:

Right now, there are six possible values:

Constant Value Apparently Required If You…
connectedDevice …use Bluetooth, Android Auto, or Android TV APIs
dataSync …perform Internet operations
location …work with LocationManager or things layered atop of it
mediaPlayback …work with audio/video APIs (e.g., MediaPlayer)
mediaProjection …capture screenshots or record screencasts with MediaProjection
phoneCall …participate in an “ongoing phone call or video conference”

It is unclear what actual code might trigger some of these. For example, it is unclear if any Internet operations require dataSync or only certain things (e.g., DownloadManager).

If your service might perform more than one of these, you can combine these values with | operators (e.g., android:foregroundServiceType="location|mediaProjection").

Audio Capture

Since Android 5.0, we have had an API for capturing screenshots and recording screencasts. However, any such screencast was a “silent film”, consisting only of video, not audio.

Android 10 introduces an official solution for capturing audio from other apps, tied to the same “media projection” system used for screenshots and screencasts.

You can learn more about MediaProjectionManager in the "The Media Projection APIs" chapter of The Busy Coder's Guide to Android Development!

Capturing Audio

There are three major requirements for your app to be able to capture audio from other apps:

  1. You will need to request the RECORD_AUDIO permission. This is a dangerous permission and therefore will require you to request it both in the manifest and at runtime via ActivityCompat.requestPermissions().
  2. If the audio capture will be performed by a service (the typical case), that service will need to be a foreground service and have its android:foregroundServiceType <service> attribute contain mediaProjection (along with any other values that you might need.
  3. You will need to obtain a MediaProjection object.

You get a MediaProjection through a slightly-annoying process:

val mgr =
  getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

startActivityForResult(mgr.createScreenCaptureIntent(), REQUEST_SCREENCAST)
override fun onActivityResult(
  requestCode: Int,
  resultCode: Int,
  data: Intent?
) {
  if (requestCode == REQUEST_SCREENCAST) {
    if (resultCode == RESULT_OK) {
      val projection = mgr.getMediaProjection(resultCode, data!!)

      // TODO something with this
    }
  }
}

Given all of that, in theory, you can:

val playbackConfig = AudioPlaybackCaptureConfiguration.Builder(projection).build()
val audioRecord = AudioRecord.Builder()
    .setAudioFormat(TODO())
    .setAudioPlaybackCaptureConfig(playbackConfig)
    .setAudioSource(TODO())
    .setBufferSizeInBytes(TODO())
    .build()

(with TODO() shown as placeholders for the rest of the AudioRecord configuration)

However, the documentation for AudioRecord is sketchy in general, and even worse with respect to audio capture. For example, it is unclear what the value should be for setAudioSource() and what the configuration should be for the AudioFormat passed to setAudioFormat().

Note that the result of this will be an audio file (or some other collection of audio bytes). If you are also trying to capture the screen using MediaProjection, this approach may give you the audio, but it will not synchronize that audio with the video, let alone put it in the same file as the video.

Note that not all apps’ audio can be captured, as we will explore in the next section.

Availability of Audio Capture

Only apps that allow audio capture can have their audio captured. In effect, apps can opt out of audio capture, just as they can use FLAG_SECURE to opt out of screen capture.

One way to block audio capture is to generate audio that is of a type that is not designed for capture. Specifically, the only audio that can be captured is audio flagged as USAGE_MEDIA, USAGE_GAME, or USAGE_UNKNOWN, referring to constants on AudioAttributes. Apps using AudioTrack for audio playback get to specify this value; other APIs might set their own value (e.g., MediaPlayer uses USAGE_MEDIA). So, if you cannot capture the audio from some app, it may be that the app — intentionally or accidentally — has specified some other usage type, such as USAGE_VOICE_COMMUNICATIONS.

In addition, an app can have an overall capture policy. The default is:

If you wish to control this more directly for your app, you can:

setAllowedCapturePolicy() has three possible values:

Deep Presses

Via methods like onTouchEvent(), you can get MotionEvent objects describing low-level interactions between the user and input devices, particularly touchscreens.

Android 10 adds a “classification” of touch event: a deep press. This is described as stemming from “the user intentionally pressing harder on the screen”.

Android Police surmises that this may lead to iOS-like “3D Touch” behavior. At present, though, it is unclear whether all hardware will support these events or if they require a special digitizer.

useEmbeddedDex

Your APK contains your code and the code from the libraries that you add to your app. That compiled code is packaged as “DEX” files representing Dalvik bytecode. Smaller apps might have a single DEX file, larger apps might have more than one (“multidex”).

On Android 4.3 and below, the Dalvik runtime would read the DEX content directly from the APK. APKs are digitally signed, so there is no way for an outside party to tamper with the code before Dalvik loads and runs it. This is great from a security standpoint but adds overhead.

On Android 4.4 and higher, the Dalvik and ART runtimes started pre-processing those DEX files, including the ahead-of-time (AOT) compilation added in Android 5.0. The output of that work is stored as ordinary files, and so processes with root privileges could tamper with them. The result is improved app performance at the cost of weakened security.

Android 10 allows you to set android:useEmbeddedDex to true in your <application> element. This tells ART to go back to Android 4.3-style approaches, avoiding the pre-processing and only using the DEX files packaged in the APK. This allows developers to opt into the tighter security, for cases where that security is worth the performance penalties (e.g., no ahead-of-time compilation).

hasFragileUserData

XDA-Developers pointed out that there is an android:hasFragileUserData flag in Android 10. This is lightly documented — we know the flag exists, but the documentation fails to indicate where the flag belongs.

According to the article, setting this to true will prompt the user whether to keep your app’s data when the system uninstalls the app. In principle, this would allow the user to get back at your data if they later re-install the app. In practice, it remains to be seen how well this works.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.