Make sure to refer to Google’s official documentation for Android Q adaptation. Here are the changes I made to Android Q:
Partition storage
Android Q new sandbox mode, each application can only access to their filtering under the view of the folder, namely the sdcard/Android/data/packagename
Description: Android Q will continue to use the READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions. Filter views are enabled by default only when targetSdkVersion>=29. To read and write sandbox files, you need to save the file to the album, etc. You still need to apply permission.WRITE_EXTERNAL_STORAGE; To read and write files outside the application, use the storage access framework
Solutions:
-
targetSdkVersion<29
-
Select Disable filtering view
<manifest . > <! -- This attribute is "false" by default on apps targeting Android Q. --> <application android:requestLegacyExternalStorage="true" . >.</application> </manifest> Copy the code
-
You are advised to store files in the filtering view. In this case, you do not need to apply for permission, but the files will be deleted after the application is uninstalled
kotlin:
// Image file val file = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) // There are many other environments, depending on the type of file you store var DIRECTORY_ALARMS = "Alarms" var DIRECTORY_AUDIOBOOKS = "Audiobooks" var DIRECTORY_DCIM = "DCIM" var DIRECTORY_DOCUMENTS = "Documents" var DIRECTORY_DOWNLOADS = "Download" var DIRECTORY_MOVIES = "Movies" var DIRECTORY_MUSIC = "Music" var DIRECTORY_NOTIFICATIONS = "Notifications" var DIRECTORY_PICTURES = "Pictures" var DIRECTORY_PODCASTS = "Podcasts" var DIRECTORY_RINGTONES = "Ringtones" var DIRECTORY_SCREENSHOTS = "Screenshots" Copy the code
java:
// Image file File file = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); // There are many other environments, depending on the type of file you store public static String DIRECTORY_ALARMS = "Alarms"; public static String DIRECTORY_AUDIOBOOKS = "Audiobooks"; public static String DIRECTORY_DCIM = "DCIM"; public static String DIRECTORY_DOCUMENTS = "Documents"; public static String DIRECTORY_DOWNLOADS = "Download"; public static String DIRECTORY_MOVIES = "Movies"; public static String DIRECTORY_MUSIC = "Music"; public static String DIRECTORY_NOTIFICATIONS = "Notifications"; public static String DIRECTORY_PICTURES = "Pictures"; public static String DIRECTORY_PODCASTS = "Podcasts"; public static String DIRECTORY_RINGTONES = "Ringtones"; public static String DIRECTORY_SCREENSHOTS = "Screenshots"; Copy the code
Data and identifier changes
Description: Starting with Android Q, An application must have the READ_PRIVILEGED_PHONE_STATE privilege to access the device’s non-reset identifier (including the IMEI and sequence number), and this privilege can only be used by the system app, that is, DeviceId cannot be obtained on Android Q
Alternative methods:
-
Android Id:
kotlin:
val androidId = Settings.Secure.getString( context.contentResolver, Settings.Secure.ANDROID_ID ) Copy the code
java:
String androidId = Settings.Secure.getString( context.contentResolver, Settings.Secure.ANDROID_ID ); Copy the code
-
However, in the actual application, it was found that Android Id acquisition failed, so the above method was improved
kotlin:
var deviceId = Settings.Secure.getString( getAppContext().contentResolver, Settings.Secure.ANDROID_ID ) if (androidId.isNullOrEmpty()) { deviceId = getUniquePsuedoID() } fun getUniquePsuedoID(a): String { val devIDShort = "35" + Build.BOARD.length % 10 + Build.BRAND.length % 10 + Build.CPU_ABI.length % 10 + Build.DEVICE.length % 10 + Build.MANUFACTURER.length % 10 + Build.MODEL.length % 10 + Build.PRODUCT.length % 10 // Android.os.build. SERIAL is available for devices with API >= 9 // http://developer.android.com/reference/android/os/Build.html#SERIAL // If the user updates the system or root their device, the API will generate duplicate records var serial: String? try { serial = android.os.Build::class.java.getField("SERIAL").get(null).toString() return UUID( devIDShort.hashCode().toLong(), serial.hashCode().toLong() ).toString() } catch (exception: Exception) { serial = "serial" } // Finally, combine the above values and generate a UUID as a unique ID returnUUID(devIDShort.hashCode().toLong(), serial!! .hashCode().toLong()).toString() }Copy the code
java:
String deviceId = Settings.Secure.getString( context.contentResolver, Settings.Secure.ANDROID_ID ); if(TextUtils.isEmpty(deviceId)) { deviceId = getUniquePsuedoID() } public String getUniquePsuedoID() { String devIDShort = "35" + Build.BOARD.length % 10 + Build.BRAND.length % 10 + Build.CPU_ABI.length % 10 + Build.DEVICE.length % 10 + Build.MANUFACTURER.length % 10 + Build.MODEL.length % 10 + Build.PRODUCT.length % 10; / / API > = 9 equipment is android. The OS. Build. The SERIAL / / / / http://developer.android.com/reference/android/os/Build.html#SERIAL If the user updates the system or root their device, the API will generate a duplicate record String serial; try { serial = android.os.Build::class.java.getField("SERIAL").get(null).toString() return UUID( devIDShort.hashCode().toLong(), serial.hashCode().toLong() ).toString(); } catch (Exception e) { serial = "serial"; Return UUID((long) devidshor.hashcode (), (long) serial.hashcode ()).tostring (); }Copy the code
Restrict the background start of the Activity
Note: This behavior change applies to all apps running on Android Q, even those targeted at Android 9 (API level 28) or lower. In addition, even if your app is targeted at Android 9 or lower and originally installed on a device running Android 9 or lower, this behavior change will take effect after the device is upgraded to Android Q.
Solutions:
Sending a full-screen notification automatically starts the Activity
kotlin:
fun sendNotification(
title: String? , body:String? .data: PushMessageNode? , bitmap:Bitmap?). {
val intent = Intent(this, PushJumpActivity::class.java)
intent.putExtra(WhatConstants.Intent.FIRE_PUSH_MESSAGE, data)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val pendingIntent = PendingIntent.getActivity(
this, requestCode, intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val notificationManager =
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
if(notificationManager ! =null) {
val notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
valmNotificationChannel = NotificationChannel( NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH ) notificationManager.createNotificationChannel(mNotificationChannel) } notificationBuilder .setSmallIcon(R.mipmap.logo) .setLargeIcon( bitmap ? : BitmapFactory.decodeResource( context, R.mipmap.logo ) ) .setContentTitle(title) .setContentText(body) .setShowWhen(true)
.setAutoCancel(true)
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
// Set it to full-screen notification. If the App is in the foreground, the notification will be suspended and Acitivity will automatically start regardless of the foreground and background
.setFullScreenIntent(pendingIntent, true)
.setContentIntent(pendingIntent)
notificationManager.notify(
requestCode /* ID of notification */, notificationBuilder.build() ) bitmap? .recycle() } }Copy the code
java:
private void sendNotification(String title, String body, PushMessageNode data, Bitmap bitmap) {
Intent intent = new Intent(this, PushJumpActivity.class);
intent.putExtra(WhatConstants.Intent.INSTANCE.getFIRE_PUSH_MESSAGE(), data);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
int requestCode = (int) (Math.random() * 1000) + 1;
PendingIntent pendingIntent = PendingIntent.getActivity(this, requestCode /* Request code */, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationManager notificationManager = null;
notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
Notification.Builder notificationBuilder;
if(notificationManager ! =null) {
if (Build.VERSION.SDK_INT >= 26) {
NotificationChannel mNotificationChannel = new NotificationChannel("1"."Channel1", NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(mNotificationChannel);
notificationBuilder = new Notification.Builder(this."1");
} else {
notificationBuilder = new Notification.Builder(this); } notificationBuilder = notificationBuilder .setSmallIcon(R.mipmap.logo) .setLargeIcon(bitmap ! =null ? bitmap : BitmapFactory.decodeResource(context.getResources(), R.mipmap.logo))
.setContentTitle(title)
.setContentText(body)
.setShowWhen(true)
.setPriority(Notification.PRIORITY_HIGH)
.setAutoCancel(true)
.setSound(defaultSoundUri)
// Set it to full-screen notification. If the App is in the foreground, the notification will be suspended and Acitivity will automatically start regardless of the foreground and background
.setFullScreenIntent(pendingIntent, true);
.setContentIntent(pendingIntent);
notificationManager.notify(requestCode /* ID of notification */, notificationBuilder.build());
if(bitmap ! =null) bitmap.recycle(); }}Copy the code
Gets clipboard data
Note: Only the default input method (IME) or the application currently in focus can access clipboard data.
This means that applications can no longer listen to clipboard data in the background, but I’m not sure about the current application in focus. In addition, during the adaptation process, there was a problem that the clipboard data could not be obtained directly in the Acitivity onCreate, but could be obtained when the button was clicked:
class SimpleActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Get the clipboard data directly
getTextFromClip()
// Clipboard has data also return ""
// Click the button to get the clipboard data
view.setOnClickListener {
getClipboardData()
// Returns normal data for the clipboard}}private fun getTextFromClip(a): String {
val clipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
if (null== clipboardManager || ! clipboardManager.hasPrimaryClip()) {return ""
}
val clipData = clipboardManager.primaryClip
if (null == clipData || clipData.itemCount < 1) {
return ""
}
val clipText = clipData.getItemAt(0)? .text ? :""
return clipText.toString()
}
}
Copy the code
View.post () is used to get the clipboard data after the view.post() is used to get the clipboard data. See that view.post() is executed after view dispatchAttachedToWindow and write it as follows:
kotlin:
/** * Gets the contents of the clipboard */
fun getClipBoardText(@Nullable activity: Activity? , f: (String) -> Unit) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && activity ! =null) {
getTextFromClipFroAndroidQ(activity, f)
} else {
f.invoke(getTextFromClip())
}
}
/** * AndroidQ gets the contents of the clipboard */
@TargetApi(Build.VERSION_CODES.Q)
private fun getTextFromClipFroAndroidQ(@NonNull activity: Activity, f: (String) -> Unit) {
val runnable = Runnable {
try {
val clipboardManager =
activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
if (null== clipboardManager || ! clipboardManager.hasPrimaryClip()) { f.invoke("")
return@Runnable
}
val clipData = clipboardManager.primaryClip
if (null == clipData || clipData.itemCount < 1) {
f.invoke("")
return@Runnable
}
val clipText = clipData.getItemAt(0)? .text ? :""
f.invoke(clipText.toString())
return@Runnable
} catch (e: Exception) {
f.invoke("")
return@Runnable
}
}
activity.registerActivityLifecycleCallbacks(object :Application.ActivityLifecycleCallbacks {
override fun onActivityPaused(activity: Activity){}override fun onActivityStarted(activity: Activity){}override fun onActivityDestroyed(activity: Activity){ activity.window? .decorView?.removeCallbacks(runnable) }override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle){}override fun onActivityStopped(activity: Activity){}override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?).{}override fun onActivityResumed(activity: Activity){ } }) activity.window? .decorView?.post(runnable) ?: f.invoke("")}private fun getTextFromClip(a): String {
try {
// You can use the Application Context
val clipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
if (null== clipboardManager || ! clipboardManager.hasPrimaryClip()) {return ""
}
val clipData = clipboardManager.primaryClip
if (null == clipData || clipData.itemCount < 1) {
return ""
}
val item = clipData.getItemAt(0) ?: return ""
valclipText = item.text ? :""
return if (TextUtils.isEmpty(clipText)) "" else clipText.toString()
} catch (e: Exception) {
return ""}}Copy the code
java:
public interface Function {
/** Invokes the function. */
void invoke(String text);
}
void getClipBoardText(@Nullable Activity activity, final Function f) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && activity ! =null) {
getTextFromClipFroAndroidQ(activity, f);
} else{ f.invoke(getTextFromClip()); }}/** * AndroidQ gets the contents of the clipboard */
@TargetApi(Build.VERSION_CODES.Q)
private void getTextFroClipFromAndroidQ(@NonNull final Activity activity, final Function f) {
Runnable runnable = new Runnable() {
@Override
public void run(a) {
ClipboardManager clipboardManager =
(ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE);
if (null== clipboardManager || ! clipboardManager.hasPrimaryClip()) { f.invoke("");
return;
}
ClipData clipData = clipboardManager.getPrimaryClip();
if (null == clipData || clipData.getItemCount() < 1) {
f.invoke("");
return;
}
ClipData.Item item = clipData.getItemAt(0);
if (item == null) {
f.invoke("");
return;
}
CharSequence clipText = item.getText();
if (TextUtils.isEmpty(clipText))
f.invoke("");
else
f.invoke(clipText.toString());
}
}
activity.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @androidx.annotation.Nullable Bundle savedInstanceState) {}@Override
public void onActivityStarted(@NonNull Activity activity) {}@Override
public void onActivityResumed(@NonNull Activity activity) {}@Override
public void onActivityPaused(@NonNull Activity activity) {}@Override
public void onActivityStopped(@NonNull Activity activity) {}@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}@Override
public void onActivityDestroyed(@NonNull Activity activity) { activity.getWindow().getDecorView().removeCallbacks(runnable); }}); activity.getWindow().getDecorView().post(runnable); }private String getTextFromClip(a) {
ClipboardManager clipboardManager =
(ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
if (null== clipboardManager || ! clipboardManager.hasPrimaryClip()) {return "";
}
ClipData clipData = clipboardManager.getPrimaryClip();
if (null == clipData || clipData.getItemCount() < 1) {
return "";
}
ClipData.Item item = clipData.getItemAt(0);
if (item == null)
return "";
CharSequence clipText = item.getText();
if (TextUtils.isEmpty(clipText))
return "";
else
return clipText.toString();
}
Copy the code
Background applications obtain user location permissions
Description: AndroidQAndroid Q introduces a new location permission ACCESS_BACKGROUND_LOCATION. You need to apply for a new permission to access the location in the background. The foreground access to the location is the same as before. For details, see Android Q Privacy Changes: Users can control the application’s access to device location information
Solutions:
<manifest>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />// Add background request location permission<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
</manifest>
Copy the code
Conclusion:
This is what I have changed in the adaptation of Android Q. It is not so clear where the clipboard data is obtained and where the application is focused.