對Android Q進行適配你們必定要參考Google官方文檔, 下面是我在作Android Q所作更改的地方:html
Android Q新加了沙盒模式, 每一個應用只能訪問本身過濾視圖下的文件夾, 即 sdcard/Android/data/packagenamejava
說明 : Android Q 會繼續使用 READ_EXTERNAL_STORAGE
和 WRITE_EXTERNAL_STORAGE
權限, 只有targetSdkVersion>=29, 纔會默認啓用過濾視圖, 而且此時無需申請權限, 便可讀寫沙盒文件, 須要將文件保存到相冊等, 依然須要申請permission.WRITE_EXTERNAL_STORAGE
; 如需讀寫應用之外的文件須要經過存儲訪問框架
android
解決方法:app
targetSdkVersion<29框架
選擇停用過濾視圖dom
<manifest ... >
<!-- This attribute is "false" by default on apps targeting Android Q. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
複製代碼
推薦方法, 將文件存儲到過濾視圖中, 此時也不須要申請權限, 可是應用卸載會把文件刪除ide
kotlin:
post
//圖片文件
val file = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
//還有如下那麼多環境, 根據存儲文件類型選擇不一樣環境
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"
複製代碼
java:
ui
//圖片文件
File file = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
//還有如下那麼多環境, 根據存儲文件類型選擇不一樣環境
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";
複製代碼
說明: 從 Android Q 開始,應用必須具備 READ_PRIVILEGED_PHONE_STATE
特許權限才能訪問設備的不可重置標識符(包含 IMEI 和序列號, 而且這個權限只有系統app纔可使用, 也就是說在Android Q上已經不能獲取DeviceId了this
替代方法:
Android Id:
kotlin:
val androidId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)
複製代碼
java:
String androidId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
);
複製代碼
可是在實際應用中發現, 有Android Id獲取失敗的狀況, 因此就完善了上面的方法
kotlin:
var deviceId = Settings.Secure.getString(
getAppContext().contentResolver,
Settings.Secure.ANDROID_ID
)
if (androidId.isNullOrEmpty()) {
deviceId = getUniquePsuedoID()
}
fun getUniquePsuedoID(): 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
// API >= 9 的設備纔有 android.os.Build.SERIAL
// http://developer.android.com/reference/android/os/Build.html#SERIAL
// 若是用戶更新了系統或 root 了他們的設備,該 API 將會產生重複記錄
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"
}
// 最後,組合上述值並生成 UUID 做爲惟一 ID
return UUID(devIDShort.hashCode().toLong(), serial!!.hashCode().toLong()).toString()
}
複製代碼
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 的設備纔有 android.os.Build.SERIAL
// http://developer.android.com/reference/android/os/Build.html#SERIAL
// 若是用戶更新了系統或 root 了他們的設備,該 API 將會產生重複記錄
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";
}
// 最後,組合上述值並生成 UUID 做爲惟一 ID
return UUID((long)devIDShort.hashCode(), (long)serial.hashCode()).toString();
}
複製代碼
說明: 此項行爲變動適用於在 Android Q 上運行的全部應用,甚至包括以 Android 9(API 級別 28)或更低版本爲目標平臺的應用。此外,即便您的應用以 Android 9 或更低版本爲目標平臺而且最初安裝在運行 Android 9 或更低版本的設備上,該行爲變動仍會在設備升級到 Android Q 後生效。
解決方法:
發送全屏通知會自動啓動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) {
val mNotificationChannel =
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))
//設置爲全屏通知, 此時若App處於前臺, 會爲懸掛通知, 不管前臺後臺, 都會自動啓動Acitivity
.setFullScreenIntent(pendingIntent, true)
.setContentIntent(pendingIntent)
notificationManager.notify(
requestCode /* ID of notification */,
notificationBuilder.build()
)
bitmap?.recycle()
}
}
複製代碼
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)
//設置爲全屏通知, 此時若App處於前臺, 會爲懸掛通知, 不管前臺後臺, 都會自動啓動Acitivity
.setFullScreenIntent(pendingIntent, true);
.setContentIntent(pendingIntent);
notificationManager.notify(requestCode /* ID of notification */, notificationBuilder.build());
if (bitmap != null)
bitmap.recycle();
}
}
複製代碼
說明: 只有默認輸入法(IME)或者是目前處於焦點的應用, 才能訪問到剪貼板數據.
這也就是說應用已經不能在後臺監聽剪貼板數據了, 不過我對目前處於焦點的應用這句話不太瞭解 . 另外在適配過程當中, 遇到了一個問題, 在Acitivity onCreate直接獲取剪貼板數據是不能成功獲取的, 而在按鈕點擊的時候是能夠的:
class SimpleActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//直接獲取剪切板數據
getTextFromClip()
//剪貼板有數據也return ""
//點擊按鈕獲取剪切板數據
view.setOnClickListener {
getClipboardData()
//返回剪貼板的正常數據
}
}
private fun getTextFromClip(): 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()
}
}
複製代碼
後面又對目前處於焦點的應用思考了一下, 應該就是視圖加載到窗口上才能獲取焦點, 後面通過適配, 在view.post()以後獲取剪貼板數據,又參考了這篇文章[Android源碼解析]view.post()到底幹了啥, 瞭解到view.post()是在view dispatchAttachedToWindow後執行的, 而後寫出方法以下:
kotlin:
/** * 獲取剪貼板的內容 */
fun getClipBoardText(@Nullable activity: Activity?, f: (String) -> Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && activity != null) {
getTextFromClipFromAndroidQ(activity, f)
} else {
f.invoke(getTextFromClip())
}
}
/** * AndroidQ 獲取剪貼板的內容 */
@TargetApi(Build.VERSION_CODES.Q)
private fun getTextFromClipFromAndroidQ(@NonNull activity: Activity, f: (String) -> Unit) {
activity.window?.decorView?.post {
try {
val clipboardManager =
activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?
if (null == clipboardManager || !clipboardManager.hasPrimaryClip()) {
f.invoke("")
return@post
}
val clipData = clipboardManager.primaryClip
if (null == clipData || clipData.itemCount < 1) {
f.invoke("")
return@post
}
val clipText = clipData.getItemAt(0)?.text ?: ""
f.invoke(clipText.toString())
return@post
} catch (e: Exception) {
f.invoke("")
return@post
}
} ?: f.invoke("")
}
private fun getTextFromClip(): String {
try {
//可使用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 ""
val clipText = item.text ?: ""
return if (TextUtils.isEmpty(clipText)) "" else clipText.toString()
} catch (e: Exception) {
return ""
}
}
複製代碼
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) {
getTextFromClipFromAndroidQ(activity, f);
} else {
f.invoke(getTextFromClip());
}
}
/** * AndroidQ 獲取剪貼板的內容 */
@TargetApi(Build.VERSION_CODES.Q)
private void getTextFromClipFromAndroidQ(@NonNull final Activity activity, final Function f) {
activity.getWindow().getDecorView().post(new Runnable() {
@Override
public void run() {
ClipboardManager clipboardManager =
(ClipboardManager)activity.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();
}
});
}
private String getTextFromClip() {
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();
}
複製代碼
說明: AndroidQAndroid Q 引入了新的位置權限 ACCESS_BACKGROUND_LOCATION
, 須要申請新權限才能後臺訪問位置, 前臺獲取位置權限與之前保持一致, 由於適配無需後臺獲取用戶位置, 因此沒有寫, 能夠參考Android Q 隱私權變動:用戶可控制應用對設備位置信息的訪問權限
解決辦法:
<manifest>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
//新增後臺請求位置權限
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
</manifest>
複製代碼
以上就是我在適配Android Q所更改的地方, 就是對於獲取剪貼板數據的地方, 具備焦點的應用, 不是那麼明確, 有了解的大佬來解釋一下, 另有錯誤歡迎指正.