老规矩,首先将咱们项目中的targetSdkVersion
改成 29。html
在Android 10以前的版本上,咱们在作文件的操做时都会申请存储空间的读写权限。可是这些权限彻底被滥用,形成的问题就是手机的存储空间中充斥着大量不明做用的文件,而且应用卸载后它也没有删除掉。为了解决这个问题,Android 10 中引入了Scoped Storage
的概念,经过添加外部存储访问限制来实现更好的文件管理。java
首先明确一个概念,外部储存和内部储存。android
内部储存:/data
目录。通常咱们使用getFilesDir()
或 getCacheDir()
方法获取本应用的内部储存路径,读写该路径下的文件不须要申请储存空间读写权限,且卸载应用时会自动删除。web
外部储存:/storage
或/mnt
目录。通常咱们使用getExternalStorageDirectory()
方法获取的路径来存取文件。缓存
由于不一样厂商、系统版本的缘由,因此上述的方法并无一个固定的文件路径。了解了上面的概念,那咱们所说的外部储存访问限制,能够认为是针对getExternalStorageDirectory()
路径下的文件。具体的规则以下表:
安全
上图将外部存储空间分为了三部分:微信
特定目录(App-specific),使用getExternalFilesDir()或 getExternalCacheDir()方法访问。无需权限,且卸载应用时会自动删除。app
照片、视频、音频这类媒体文件。使用MediaStore 访问,访问其余应用的媒体文件时须要READ_EXTERNAL_STORAGE权限。框架
其余目录,使用存储访问框架SAF(Storage Access Framwork)ide
因此在Android 10上即便你拥有了储存空间的读写权限,也没法保证能够正常的进行文件的读写操做。
最简单粗暴的方法就是在AndroidManifest.xml
中添加 android:requestLegacyExternalStorage="true"
来请求使用旧的存储模式。
可是我不推荐此方法。由于在下一个版本的Android中,此条配置将会失效,将强制采用外部储存限制。其实早在Android Q Beta 3以前都是强制的,但为了给开发者适配的时间才没有强制执行。因此若是你不抓住这段时间去适配,那么今年下半年出了Android 11。。。直接开花~~
若是你已经适配Android 10,这里有个现象要注意一下:
若是应用经过升级安装,那么还会使用之前的储存模式(Legacy View)。只有经过首次安装或是卸载从新安装才能启用新模式(Filtered View)。
因此在适配时,咱们的判断代码以下:
// 使用Environment.isExternalStorageLegacy()来检查APP的运行模式 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()) { }
这样的好处是你能够在用户升级后,能方便的将用户的数据移动至应用的特定目录。不然你只能经过SAF去移动,这样会很是麻烦。若是你要移动数据注意只适用于Android 10下,因此如今适配反而是一个好时机。固然若是你不须要迁移数据,那适配会更省事。
下面就说说推荐适配方案:
之前咱们习惯使用Environment.getExternalStorageDirectory()方法,那么如今可使用getExternalFilesDir()方法(包括下载的安装包这类的文件)。若是是缓存类型文件,能够放到getExternalCacheDir()路径下。
或者使用MediaStore,将文件存至对应的媒体类型中(图片:MediaStore.Images ,视频:MediaStore.Video,音频:MediaStore.Audio),不过仅限于多媒体文件。
下面代码将图片保存到公共目录下,返回Uri:
public static Uri createImageUri(Context context) { ContentValues values = new ContentValues(); // 须要指定文件信息时,非必须 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); }
java.io.FileNotFoundException: open failed: EACCES (Permission denied)
好比我在适配项目中使用的图片选择器时,首先修改了Glide 经过加载File的方式显示图片。改成加载Uri的方式,不然图片没法显示出来。
Uri的获取方式仍是使用MediaStore:
String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)); Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
其次为了便于不影响以前选择图片返回File的逻辑(由于通常都是上传File,没有直接上传Uri的操做),因此我将最终选择的文件又转存进了getExternalFilesDir(),主要代码以下:
File imgFile = this.getExternalFilesDir("image"); if (!imgFile.exists()){ imgFile.mkdir(); } try { File file = new File(imgFile.getAbsolutePath() + File.separator + System.currentTimeMillis() + ".jpg"); // 使用openInputStream(uri)方法获取字节输入流 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(); // 文件可用新路径 file.getAbsolutePath() } catch (Exception e) { e.printStackTrace(); }
Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getString(idColumnIndex)); final double[] latLong; // 从ExifInterface类获取位置信息 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]; }
这样下来,一个图片选择器就基本适配完了。
应用在卸载后,会将App-specific目录下的数据删除,若是在AndroidManifest.xml中声明:android:hasFragileUserData="true"用户能够选择是否保留。
对于SAF的使用,能够查看我以前写的SAF使用攻略,这里就不展开说了。
最后这里有一个介绍Scoped Storage的视频,推荐观看:
准备好使用分区存储 | ADS 中文字幕视频
准备好使用分区存储
从6.0开始,基本每次都会有权限方面变更,此次也不例外。(前几天发布了Android 11的预览版,看来也有权限方面的变化。。。单次权限即将到来)
Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限(危险权限)。
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
该权限容许应用程序在后台访问位置。若是请求此权限,则还必须请求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限。只请求此权限无效果。
在Android 10的设备上,若是你的应用的 targetSdkVersion < 29,则在请求ACCESS_FINE_LOCATION 或ACCESS_COARSE_LOCATION权限时,系统会自动同时请求ACCESS_BACKGROUND_LOCATION。在请求弹框中,选择“始终容许”表示赞成后台获取位置信息,选择“仅在应用使用过程当中容许”或"拒绝"选项表示拒绝受权。
若是你的应用的 targetSdkVersion >= 29,则请求ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION权限表示在前台时拥有访问设备位置信息的权。在请求弹框中,选择“始终容许”表示先后台均可以获取位置信息,选择“仅在应用使用过程当中容许”只表示拥有前台的权限。
总结一下就是下图:
其实官方不推荐你使用申请后台访问权的方式,由于这样的结果无非就是多请求一个权限,那么这像变动还有什么意义?申请过多的权限,也会形成用户的反感。因此官方推荐使用前台服务来实现,在前台服务中获取位置信息。
<service android:name="MyNavigationService" android:foregroundServiceType="location" ... > ... </service>
boolean permissionApproved = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; if (permissionApproved) { // 启动前台服务 } else { // 请求前台访问位置权限 }
如此一来就能够在Service中获取位置信息。
下面列举了Android 10中必须具备 ACCESS_FINE_LOCATION 权限才能使用类和方法:
Android 10新增权限,上面有提到,不赘述了。
Android 10上该权限已废弃。
简单解释就是应用处于后台时,没法启动Activity。好比点开一个应用会进入启动页或者广告页,通常会有几秒的延时再跳转至首页。若是这期间你退到后台,那么你将没法看到跳转过程。而在以前的版本中,会强制弹出页面至前台。
既然是限制,那么确定有不受限的状况,主要有如下几点:
应用具备可见窗口,例如前台 Activity。
应用在前台任务的返回栈中已有的 Activity。
应用在 Recents 上现有任务的返回栈中已有的 Activity。Recents 就是咱们的任务管理列表。
应用收到系统的 PendingIntent 通知。
应用收到它应该在其中启动界面的系统广播。示例包括 ACTION_NEW_OUTGOING_CALL 和 SECRET_CODE_ACTION。应用可在广播发送几秒钟后启动 Activity。
用户已向应用授予 SYSTEM_ALERT_WINDOW 权限,或是在应用权限页开启后台弹出页面的开关。
由于此项行为变动适用于在 Android 10 上运行的全部应用,因此这一限制致使最明显的问题就是点击推送信息时,有些应用没法进行正常的跳转(具体的实现问题致使)。因此针对这类问题,能够采起PendingIntent的方式,发送通知时使用setContentIntent方法。
固然你也能够申请相应权限或者白名单:
不过申请白名单这种方法受各类手机厂商所限,很麻烦。感受还不如引导用户手动开启权限。。。
对于全屏 intent,注意设置最高优先级和添加USE_FULL_SCREEN_INTENT权限,这是一个普通权限。好比微信来语音或者视频通话时,弹出的接听页面就是使用这一功能。
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
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) // <--- 高优先级 .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 Notification incomingCallNotification = notificationBuilder.build();
注意:在部分手机上,直接设置setPriority无效(或者说以渠道优先级为准)。因此须要建立通知渠道时将重要性设置为IMPORTANCE_HIGH。
NotificationChannel channel = new NotificationChannel(channelId, "xxx", NotificationManager.IMPORTANCE_HIGH);
后台启动 Activity 的限制的目的是为了减小对用户操做的中断。若是你有要弹出的页面,推荐你先弹出通知,让用户本身选择接下来的操做,而不是一股脑的强制弹出。(若是你的全屏intent都让用户反感,那他也能够关掉你的通知,不至于任你摆布。)
Android 10 新增了一个系统级的深色主题(在系统设置中开启)。虽然深色主题并非强制适配项,可是它能够带给用户更好的体验:
可大幅减小耗电量。 OLED 屏幕中每一个像素都是自主发光,因此在显示深色元素时像素所消耗的电流更低,尤为在纯黑颜色时像素点能够彻底关闭来达到省电的效果。
为弱视以及对强光敏感的用户提升可视性。深色能够下降屏幕的总体视觉亮度,减小对眼睛的视觉压力。
让全部人均可以在光线较暗的环境中更轻松地使用设备。
适配方法有两种:
官方文档中提到的继承Theme.AppCompat.DayNight 或者 Theme.MaterialComponents.DayNight的方法,但这只是将咱们使用的各类View的默认样式进行了适配,并不太适用于实际项目的适配。由于具体的项目中的View都按照设计的风格进行了重定义。
其实适配的方法很简单,相似屏幕适配、国际化的操做,并不须要继承上面的主题。好比你要修改颜色,就在res 下新建 values-night目录,建立对应的colors.xml文件。将具体要修改的色值定义在里面。图标之类的也是一个思路,建立对应的 drawable-night目录。
只要你以前的代码不是硬编码且代码规范,那么适配起来仍是很轻松。
Android 10 提供 Force Dark 功能。一如其名,此功能可以让开发者快速实现深色主题背景,而无需明确设置 DayNight 主题背景。
若是您的应用采用浅色主题背景,则 Force Dark 会分析应用的每一个视图,并在相应视图在屏幕上显示以前,自动应用深色主题背景。有些开发者会混合使用 Force Dark 和本机实现,以缩短实现深色主题背景所需的时间。
应用必须选择启用 Force Dark,方法是在其主题背景中设置 android:forceDarkAllowed=“true”。此属性会在全部系统及 AndroidX 提供的浅色主题背景(例如 Theme.Material.Light)上设置。使用 Force Dark 时,您应确保全面测试应用,并根据须要排除视图。
若是您的应用使用Dark Theme主题(例如Theme.Material),则系统不会应用 Force Dark。一样,若是应用的主题背景继承自 DayNight 主题(例如Theme.AppCompat.DayNight),则系统不会应用 Force Dark,由于会自动切换主题背景。
您能够经过 android:forceDarkAllowed 布局属性或 setForceDarkAllowed(boolean) 在特定视图上控制 Force Dark。
上述内容我直接照搬文档的说明。总结一下,使用Force Dark须要注意几点:
若是使用的是 DayNight 或 Dark Theme 主题,则设置forceDarkAllowed 不生效。
若是有须要排除适配的部分,能够在对应的View上设置forceDarkAllowed为false。
这里说说我实际使用此方法的感觉:总体仍是不错的,设置的色值会自动取反。但也所以颜色不受控制,可否达到预期效果是个须要注意的问题。追求快速适配能够采起此方案。
使用 AppCompatDelegate.setDefaultNightMode(@NightMode int mode)方法,其中参数mode有如下几种:
下面的代码是官方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; } } } }
经过AppCompatDelegate.getDefaultNightMode()方法,能够获取到当前的模式,这样便于代码中去适配。
首先在清单文件中给对应的Activity配置 android:configChanges=“uiMode”:
<activity android:name=".MyActivity" android:configChanges="uiMode" />
这样在onConfigurationChanged方法中就能够获取:
@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: // 关闭 break; case Configuration.UI_MODE_NIGHT_YES: // 开启 break; default: break; } }
详细的内容你能够参看官方文档和官方Demo。
其实和上面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; }
对不可重置的设备标识符实施了限制
受影响的方法包括:
若是你的应用没有该权限,却仍然使用了以上的方法,则返回的结果会因目标 SDK 版本而异:
这项改动表示第三方应用没法获取Device ID这类惟一标识。若是你须要惟一标识符,请参阅文档:惟一标识符的最佳作法。
固然你也能够试试移动安全联盟(MSA)联合多家厂商共同开发的统一补充设备标识调用SDK。听说还有点不稳定,由于我暂时尚未尝试过,因此不作评价。
除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,不然它没法访问 Android 10 或更高版本平台上的剪贴板数据。
以 Android 10 或更高版本为目标平台的应用没法启用或停用 WLAN。WifiManager.setWifiEnabled()方法始终返回 false。
若是您须要提示用户启用或停用 WLAN,请使用设置面板。
Android10上对折叠屏设备有了更好的支持,对于有折叠屏适配的需求,能够参看为可折叠设备构建应用 和 华为折叠屏应用开发指导。
以上内容只是Android 10中比较大的几项变化,完整的内容能够查看官方文档。