Compared to the Android 9 adaptation I wrote last year, Android 10 is a bit heavy. Did not expect to write my whole two days, vomiting blood in…

The preparatory work

As usual, first change the targetSdkVersion in our project to 29.

1.Scoped Storage

instructions

Prior to Android 10, we applied for read and write access to the storage space when we did files. However, these permissions are completely abused, and the problem is that the phone’s storage space is filled with files of unknown use, and it doesn’t delete the application when it is uninstalled. To address this, Android 10 introduced the concept of Scoped Storage, which adds restrictions on access to external Storage for better file management.

First define a concept, external storage and internal storage.

  • Internal storage: /data directory. Generally, you can use getFilesDir() or getCacheDir() to obtain the internal storage path of the application. You do not need to apply for the read/write permission on the storage space for the files in this path, and the files will be automatically deleted when the application is uninstalled.

  • External storage: /storage or/MNT directories. Generally we use external.getexternalstoragedirectory () method to obtain path to access files.

The above method does not have a fixed file path due to different vendors and system versions. Understand the concept of the above, that what we call the external storage access restrictions, can be thought of as for external.getexternalstoragedirectory () the path of files. The specific rules are as follows:

  • Specific directories (app-specific), accessed using the getExternalFilesDir() or getExternalCacheDir() methods. No permission is required, and the application is automatically deleted after uninstallation.

  • Media files such as photos, videos, and audio. If the MediaStore is used, the READ_EXTERNAL_STORAGE permission is required to access the media files of other applications.

  • For other directories, use the Storage Access Framework (SAF)

So on Android 10, even if you have access to the storage space, there is no guarantee that you can read and write files properly.

adapter

The most simple and crude method is in AndroidManifest. Add android: XML requestLegacyExternalStorage = “true” to request to use the old storage mode.

But I don’t recommend this approach. Because in the next version of Android, this configuration will be invalid and external storage restrictions will be enforced. It was mandatory before Android Q Beta 3, but it was not enforced to give developers time to adapt. So if you don’t take the time to adapt, Android 11 will come out later this year… Direct flowering

If you’re already on Android 10, here’s what to watch out for:

If the application was installed through an upgrade, the previous storage mode (Legacy View) is used. The new mode (Filtered View) can only be enabled after the first installation or after uninstallation.

Therefore, in adaptation, our judgment code is as follows:

    / / use the Environment. IsExternalStorageLegacy () to check the APP running mode
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && ! Environment.isExternalStorageLegacy()) { }Copy the code

The benefit of this is that you can easily move the user’s data to a specific directory in your app after they upgrade. Otherwise you can only move through SAF, which can be very troublesome. If you’re moving data, note that it’s only available with Android 10, so now is a good time to adapt. Of course, if you don’t need to migrate the data, then adaptation is much easier.

Here’s the recommended adaptation:

  • Change your file path for the file operations involved in your application.

Before we got used to the Environment. External.getexternalstoragedirectory () method, so you can now use getExternalFilesDir () method (including installation package to download these files). If it is a cache type file, you can put it in the getExternalCacheDir() path.

Or use MediaStore, which stores files in the corresponding media type (Images: mediastore.images, Video: mediastore.video, Audio: mediastore.audio), but only for multimedia files.

The following code saves the image to a public directory and returns the Uri:

   public static Uri createImageUri(Context context) {
        ContentValues values = new ContentValues();
        // It is not required to specify file information
        values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
        values.put(MediaStore.Images.Media.DISPLAY_NAME, "Image.png");
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
        values.put(MediaStore.Images.Media.TITLE, "Image.png");
        values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/test");
        
        return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
    }
Copy the code
  • Access to media resources: scenes such as image selectors. Instead of using File directly, use URIs. Otherwise, the following error is reported:
java.io.FileNotFoundException: open failed: EACCES (Permission denied)
Copy the code

For example, when I adapted the image selector used in the project, I first modified Glide to display the image by loading File. Change the way the Uri is loaded, otherwise the image will not be displayed.

The Uri is obtained using the MediaStore:

String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));

Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
Copy the code

Secondly, in order not to affect the logic of selecting image to return File (because File is generally uploaded, there is no operation of directly uploading Uri), I will finally select the File and save it into getExternalFilesDir(), the main code is as follows:

    File imgFile = this.getExternalFilesDir("image");
    if(! imgFile.exists()){ imgFile.mkdir(); }try {
        File file = new File(imgFile.getAbsolutePath() + File.separator + 
        	System.currentTimeMillis() + ".jpg");
        // Get the byte input stream using the openInputStream(URI) method
        InputStream fileInputStream = getContentResolver().openInputStream(uri);
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        byte[] buffer = new byte[1024];
        int byteRead;
        while (-1! = (byteRead = fileInputStream.read(buffer))) { fileOutputStream.write(buffer,0, byteRead);
        }
        fileInputStream.close();
        fileOutputStream.flush();
        fileOutputStream.close();
        // The file is available in a new path file.getabSolutePath ()
    } catch (Exception e) {
        e.printStackTrace();        
    }
Copy the code
  • If you want to get geolocation information in a picture, you need to applyACCESS_MEDIA_LOCATIONPermissions, and use the MediaStore. SetRequireOriginal (). Here is the official sample code:
    Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
		 cursor.getString(idColumnIndex));

    final double[] latLong;

    // Get the location information from the ExifInterface class
    photoUri = MediaStore.setRequireOriginal(photoUri);
    InputStream stream = getContentResolver().openInputStream(photoUri);
    if(stream ! =null) {
        ExifInterface exifInterface = new ExifInterface(stream);
        double[] returnedLatLong = exifInterface.getLatLong();

        // If lat/long is null, fall back to the coordinates (0, 0).latLong = returnedLatLong ! =null ? returnedLatLong : new double[2];

        // Don't reuse the stream associated with the instance of "ExifInterface".
        stream.close();
    } else {
        // Failed to load the stream, so return the coordinates (0, 0).
        latLong = new double[2];
    }
Copy the code

In this way, an image selector is basically fit.

supplement

Application after unloading, App – specific data in the directory will be deleted, if in the AndroidManifest. In the XML declaration: android: hasFragileUserData = “true” the user can choose whether to retain.

For the use of SAF, you can check out the SAF use guide I wrote earlier, but I won’t go into it here.

Finally, here’s a video about Scoped Storage. I recommend watching it:

2. Permissions change

Starting from 6.0, basically every time there will be permission changes, this is no exception. We released a preview version of Android 11 a few days ago, and it looks like there will also be permissions changes… Single permission coming soon)

1. Permission is required to access the device location information in the background

Android 10 introduces the ACCESS_BACKGROUND_LOCATION permission (dangerous permission).

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
Copy the code

This permission allows applications to access the location in the background. If this permission is requested, you must also request the ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permission. Requesting this permission alone has no effect.

On Android 10 devices, if your app’s targetSdkVersion is < 29, when requesting ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permissions, The system automatically requests ACCESS_BACKGROUND_LOCATION. In the request dialog box, select “Always allow” to allow the background to obtain the location information, and select “only allow during application use” or “deny” option to deny authorization.

If your app’s targetSdkVersion >= 29, then request ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permission to access device location information while in the foreground. In the request dialog box, select Always Allow to obtain the location information in the foreground and background. Select Only during application use allow to obtain the permission in the foreground.

To summarize, it is the following picture:

Applying for background access is not recommended
The front desk service

  1. First in the list of the correspondingserviceaddandroid:foregroundServiceType="location":
    <service
        android:name="MyNavigationService"
        android:foregroundServiceType="location" . >.</service>
Copy the code
  1. Check whether you have the foreground access permission before starting the foreground service:
    boolean permissionApproved = ActivityCompat.checkSelfPermission(this, 
		Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;

    if (permissionApproved) {
       // Start the foreground service
    } else {
       // Request foreground to access location permission
    }
    
Copy the code

In this way, you can get the location information in the Service.

2. Some phone, Bluetooth, and WLAN apis require precise location permissions

In Android 10, you must have the ACCESS_FINE_LOCATION permission to use classes and methods:

The phone

  • TelephonyManager
    • getCellLocation()
    • getAllCellInfo()
    • requestNetworkScan()
    • requestCellInfoUpdate()
    • getAvailableNetworks()
    • getServiceState()
  • TelephonyScanManager
    • requestNetworkScan()
  • TelephonyScanManager.NetworkScanCallback
    • onResults()
  • PhoneStateListener
    • onCellLocationChanged()
    • onCellInfoChanged()
    • onServiceStateChanged()

WLAN

  • WifiManager
    • startScan()
    • getScanResults()
    • getConnectionInfo()
    • getConfiguredNetworks()
  • WifiAwareManager
  • WifiP2pManager
  • WifiRttManager

bluetooth

  • BluetoothAdapter
    • startDiscovery()
    • startLeScan()
  • BluetoothAdapter.LeScanCallback
  • BluetoothLeScanner
    • startScan()

Based on the specific classes and methods provided above, we can check whether they are used in the adaptation project and handle them in a timely manner.

3.ACCESS_MEDIA_LOCATION

Android 10 new permissions, mentioned above, not to repeat.

4.PROCESS_OUTGOING_CALLS

This permission is invalid on Android 10.

3. Restrictions on background startup activities

The simple explanation is that an Activity cannot be started while the application is in the background. Clicking on an app, for example, takes you to a startup page or an AD page, usually with a delay of a few seconds before moving to the home page. If you go back to the background during this time, you won’t be able to see the jump. In previous versions, the pop-up page was forced to the foreground.

Since it is restricted, there must be unrestricted cases, mainly including the following points:

  • An application has a visible window, such as a foreground Activity.

  • Apply an existing Activity in the return stack of a foreground task.

  • Apply an existing Activity in the return stack of an existing task on the Recents. Recents are our task management list.

  • The application receives a PendingIntent notification from the system.

  • The application receives a system broadcast in which it should launch the interface. Examples include ACTION_NEW_OUTGOING_CALL and SECRET_CODE_ACTION. An application can start an Activity a few seconds after the broadcast is sent.

  • The SYSTEM_ALERT_WINDOW permission has been granted to the application, or the background pop-up page has been enabled on the application permission page.

Since this behavior change applies to all apps running on Android 10, the most obvious problem with this limitation is that some apps don’t jump properly when a push message is clicked (due to implementation issues). Therefore, to solve this problem, we can adopt the PendingIntent method and use the setContentIntent method when sending notifications.

Of course, you can also apply for corresponding permissions or whitelist:

However, the application for whitelist this method is limited by various mobile phone manufacturers, very troublesome. It feels better to guide the user to manually open the permission…

For full-screen intents, pay attention to setting the highest priority and adding the USE_FULL_SCREEN_INTENT privilege, which is a normal privilege. For example, when wechat voice or video call, popup answer page is to use this function.

    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
Copy the code
    Intent fullScreenIntent = new Intent(this, CallActivity.class);
    PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this.0,
            fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);

    NotificationCompat.Builder notificationBuilder =
            new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.drawable.notification_icon)
        .setContentTitle("Incoming call")
        .setContentText("(919) 555-1234")
        .setPriority(NotificationCompat.PRIORITY_HIGH) // <-- high priority
        .setCategory(NotificationCompat.CATEGORY_CALL)

        // Use a full-screen intent only for the highest-priority alerts where you
        // have an associated activity that you would like to launch after the user
        // interacts with the notification. Also, if your app targets Android 10
        // or higher, you need to request the USE_FULL_SCREEN_INTENT permission in
        // order for the platform to invoke this notification.
        .setFullScreenIntent(fullScreenPendingIntent, true); // < intent > < intent

    Notification incomingCallNotification = notificationBuilder.build();
Copy the code

Note: On some phones, setting setPriority is invalid (or based on channel priority). So set importance to IMPORTANCE_HIGH when you need to create a notification channel.

NotificationChannel channel = new NotificationChannel(channelId, "xxx", NotificationManager.IMPORTANCE_HIGH);
Copy the code

The purpose of the restrictions on background startup activities is to reduce interruptions to user operations. If you have a page to pop up, it is recommended that you pop up the notification first and let the user choose what to do next, rather than forcing it to pop up all at once. (If your full-screen intents are annoying, the user can turn off your notifications instead of being pushed around by you.)

4. Dark themes

Android 10 has added a system-level dark theme (enabled in system Settings). While dark themes are not mandatory, they can give users a better experience:

  • Power consumption can be significantly reduced. Each pixel in an OLED screen emits light on its own, so it consumes less current to display dark elements, especially in pure black colors, where pixels can be turned off completely to save power.

  • Improves visibility for users with amblyopia and strong light sensitivity. Dark colors can reduce the overall visual brightness of the screen, reducing visual stress on the eyes.

  • Make it easier for everyone to use equipment in low-light environments.

There are two adaptation methods:

1. Manual adaptation (resource replacement)

Official document referred to in the inheritance Theme. AppCompat. DayNight or Theme. MaterialComponents. DayNight method, but this is only will we use the default styles for the adaptation of the View, the adapter is not applicable to actual project. This is because the views in the specific project are redefined according to the design style.

In fact, the adaptation method is very simple, similar to screen adaptation, internationalization operation, do not need to inherit the above theme. For example, if you want to change the color, create a new values-night directory under res and create the corresponding colors.xml file. Specify the color value to be modified inside. Another idea is to create a drawable night directory.

As long as your previous code is not hardcoded and the code is canonical, it’s easy to fit.

2. Automatic adaptation (Force Dark)

Android 10 offers Force Dark. As the name suggests, this feature allows developers to quickly implement dark themed backgrounds without having to explicitly set up a DayNight themed background.

If your app has a light themed background, Force Dark analyzes each view of your app and automatically applies a Dark themed background before the corresponding view is displayed on the screen. Some developers use a mix of Force Dark and native implementations to reduce the time needed to implement Dark themed backgrounds.

Apps must choose to enable Force Dark by setting Android :forceDarkAllowed=”true” in the background of their theme. This property will be set on all systems and any Light Theme backgrounds AndroidX provides (such as theme.material.Light). When using Force Dark, you should be sure to test your application thoroughly and exclude views as needed.

If your application uses a Dark Theme Theme (such as theme.material), Force Dark will not be applied. Similarly, if the application’s Theme background is inherited from the DayNight Theme (for example, theme.appCompat.daynight), the system will not apply Force Dark because the Theme background is automatically switched.

You can control ForceDark on a particular view by using the Android :forceDarkAllowed layout property or setForceDarkAllowed(Boolean).

Above I directly copy the documentation instructions. To summarize, there are a few things to note when using Force Dark:

  • If DayNight or Dark Theme is used, forceDarkAllowed does not take effect.

  • If you need to exclude adaptation, you can set forceDarkAllowed to false on the corresponding View.

Here’s what I felt when I actually used this method: overall it was fine, and the color values were automatically reversed. But also because the color is not controlled, can achieve the desired effect is a problem that needs to pay attention to. This scheme can be adopted for fast adaptation.


Manually switching themes

Using AppCompatDelegate. SetDefaultNightMode (@ NightMode int mode) method, the parameter mode has the following kinds:

  • A light –MODE_NIGHT_NO
  • The dark –MODE_NIGHT_YES
  • Set by power saving mode –MODE_NIGHT_AUTO_BATTERY
  • System default –MODE_NIGHT_FOLLOW_SYSTEM

The following code is used as an example in the official Demo:

public class ThemeHelper {

    public static final String LIGHT_MODE = "light";
    public static final String DARK_MODE = "dark";
    public static final String DEFAULT_MODE = "default";

    public static void applyTheme(@NonNull String themePref) {
        switch (themePref) {
            case LIGHT_MODE: {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                break;
            }
            case DARK_MODE: {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                break;
            }
            default: {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
                } else {
                    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
                }
                break; }}}}Copy the code

Through AppCompatDelegate. GetDefaultNightMode () method, which can get to the current mode, this code to facilitate adaptation.

Listen for dark subject to be enabled

Android :configChanges=”uiMode” android:configChanges=”uiMode”

    <activity
    	android:name=".MyActivity"
    	android:configChanges="uiMode" />
Copy the code

In this way, in the onConfigurationChanged method, you can get:

	@Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
        switch (currentNightMode) {
            case Configuration.UI_MODE_NIGHT_NO:
                / / close
                break;
            case Configuration.UI_MODE_NIGHT_YES:
                / / open
                break;
            default:
                break; }}Copy the code

You can see the official document and official Demo for details.

Determine if the dark theme is on

This is the same as onConfigurationChanged:

    public static boolean isNightMode(Context context) {
        int currentNightMode = context.getResources().getConfiguration().uiMode & 
        	Configuration.UI_MODE_NIGHT_MASK;
        return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
    }

Copy the code

Identifiers and data

Restrictions are imposed on non-resettable device identifiers

Affected methods include:

  • Build
    • getSerial()
  • TelephonyManager
    • getImei()
    • getDeviceId()
    • getMeid()
    • getSimSerialNumber()
    • getSubscriberId()

Starting with Android 10, applications must have the READ_PRIVILEGED_PHONE_STATE privilege to use these methods properly.

If your application does not have this permission, but still uses the above method, the results returned will vary depending on the target SDK version:

  • If you apply theAndroid 10 or higher is the target platform, will occurSecurityException.
  • If you apply theAndroid 9 (API level 28) or lower is the target platform, the corresponding method returns null or placeholder data if the application has oneREAD_PHONE_STATEPermissions). Otherwise, it will happenSecurityException.

This change means that third-party applications cannot obtain unique identifiers such as Device IDS. If you need unique identifiers, see the documentation: Best Practices for Unique Identifiers.

Of course, you can also try the Mobile Security Alliance (MSA) joint development of a number of manufacturersCall the SDK with a unified supplementary device id. It is said to be a little unstable, because I haven’t tried it yet, so I won’t comment.

Restricted access to the clipboard data

Unless your app is the default input Method (IME) or is currently in focus, it cannot access the clipboard data on Android 10 or later.

Restrictions are imposed on enabling and disabling WLAN

WLAN cannot be enabled or disabled for applications targeting Android 10 or later. WifiManager. SetWifiEnabled () method always returns false.

If you need to prompt users to enable or disable WLAN, use the Settings panel.

6. Other

  • Android10 has better support for foldable devices. For more information on foldable adaptation, see building apps for foldable devices and huawei foldable app development guide.

  • These are just a few of the big changes in Android 10, and you can check out the official documentation for the full list.

Finally, give it a “like”

reference

  • Oppo-provides guidance on compatibility and adaptation of Android Q applications

  • Android 10 for developers

  • Teach you how to quickly adapt “dark mode” with alibaba APP example