当前位置:   article > 正文

android10的适配_use_full_screen_intent

use_full_screen_intent

市场上android10的用户越来越多了,作为资深的安卓开发人员,如果不及时给你的项目适配,将会出现很多问题,相比较去年的写的Android 9适配,这次Android 10的内容有点多,我整整写了一个星期,痛苦中。。。

 

首先将我们项目中的targetSdkVersion改为 29。

1、Scoped Storage(分区存储)

 

说明

 

在Android 10之前的版本上,我们在做文件的操作时都会申请存储空间的读写权限。但是这些权限完全被滥用,造成的问题就是手机的存储空间中充斥着大量不明作用的文件,并且应用卸载后它也没有删除掉。

 

为了解决这个问题,Android 10 中引入了Scoped Storage 的概念,通过添加外部存储访问限制来实现更好的文件管理。

 

首先明确一个概念,外部储存内部储存

 

内部储存:/data 目录。一般我们使用getFilesDir() 或 getCacheDir() 方法获取本应用的内部储存路径,读写该路径下的文件不需要申请储存空间读写权限,且卸载应用时会自动删除。

 

外部储存:/storage 或 /mnt 目录。一般我们使用getExternalStorageDirectory()方法获取的路径来存取文件。

 

因为不同厂商、系统版本的原因,所以上述的方法并没有一个固定的文件路径。了解了上面的概念,那我们所说的外部储存访问限制,可以认为是针对getExternalStorageDirectory()路径下的文件。

 

具体的规则如下表:

上图将外部存储空间分为了三部分:

 

  • 特定目录(App-specific),使用getExternalFilesDir()或 getExternalCacheDir()方法访问。无需权限,且卸载应用时会自动删除。

  • 照片、视频、音频这类媒体文件。使用MediaStore 访问,访问其他应用的媒体文件时需要READ_EXTERNAL_STORAGE权限。

  • 其他目录,使用存储访问框架SAF(Storage Access Framwork)

    https://developer.android.google.cn/guide/topics/providers/document-provider?hl=zh_cn

 

所以在Android 10上即使你拥有了储存空间的读写权限,也无法保证可以正常的进行文件的读写操作。

适配

 

最简单粗暴的方法就是在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"来请求使用旧的存储模式。

 

但是我不推荐此方法。

 

因为在下一个版本的Android中,此条配置将会失效,将强制采用外部储存限制。其实早在Android Q Beta 3之前都是强制的,但为了给开发者适配的时间才没有强制执行。所以如果你不抓住这段时间去适配,那么今年下半年出了Android 11。。。直接开花~~

 

如果你已经适配Android 10,这里有个现象要注意一下:

 

如果应用通过升级安装,那么还会使用以前的储存模式(Legacy View)。只有通过首次安装或是卸载重新安装才能启用新模式(Filtered View)。

 

所以在适配时,我们的判断代码如下:

 

  1.     // 使用Environment.isExternalStorageLegacy()来检查APP的运行模式
  2.     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && 
  3.        !Environment.isExternalStorageLegacy()) {
  4.     }

 

这样的好处是你可以在用户升级后,能方便的将用户的数据移动至应用的特定目录。

 

否则你只能通过SAF去移动,这样会非常麻烦。如果你要移动数据注意只适用于Android 10下,所以现在适配反而是一个好时机。

 

当然如果你不需要迁移数据,那适配会更省事。

 

下面就说说推荐适配方案:

 

对于应用中涉及的文件操作,修改一下你的文件路径。

 

以前我们习惯使用Environment.getExternalStorageDirectory()方法,那么现在可以使用getExternalFilesDir()方法(包括下载的安装包这类的文件)。如果是缓存类型文件,可以放到getExternalCacheDir()路径下。

 

或者使用MediaStore,将文件存至对应的媒体类型中(图片:MediaStore.Images ,视频:MediaStore.Video,音频:MediaStore.Audio),不过仅限于多媒体文件。

 

下面代码将图片保存到公共目录下,返回Uri:

 

  1.    public static Uri createImageUri(Context context) {
  2.         ContentValues values = new ContentValues();
  3.         // 需要指定文件信息时,非必须
  4.         values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
  5.         values.put(MediaStore.Images.Media.DISPLAY_NAME, "Image.png");
  6.         values.put(MediaStore.Images.Media.MIME_TYPE"image/png");
  7.         values.put(MediaStore.Images.Media.TITLE, "Image.png");
  8.         values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/test");
  9.         return context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
  10.     }

 

对于媒体资源的访问:比如图片选择器这类的场景。无法直接使用File,而应使用Uri。否则报错如下:

 

java.io.FileNotFoundException: open failed: EACCES (Permission denied)

 

比如我在适配项目中使用的图片选择器时,首先修改了Glide 通过加载File的方式显示图片。改为加载Uri的方式,否则图片无法显示出来。

 

Uri的获取方式还是使用MediaStore:

 

  1. String id = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
  2. Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);

 

其次为了便于不影响之前选择图片返回File的逻辑(因为一般都是上传File,没有直接上传Uri的操作),所以我将最终选择的文件又转存进了getExternalFilesDir(),主要代码如下:

 

  1.     File imgFile = this.getExternalFilesDir("image");
  2.     if (!imgFile.exists()){
  3.         imgFile.mkdir();
  4.     }
  5.     try {
  6.         File file = new File(imgFile.getAbsolutePath() + File.separator + 
  7.             System.currentTimeMillis() + ".jpg");
  8.         // 使用openInputStream(uri)方法获取字节输入流
  9.         InputStream fileInputStream = getContentResolver().openInputStream(uri);
  10.         FileOutputStream fileOutputStream = new FileOutputStream(file);
  11.         byte[] buffer = new byte[1024];
  12.         int byteRead;
  13.         while (-1 != (byteRead = fileInputStream.read(buffer))) {
  14.             fileOutputStream.write(buffer, 0, byteRead);
  15.         }
  16.         fileInputStream.close();
  17.         fileOutputStream.flush();
  18.         fileOutputStream.close();
  19.         // 文件可用新路径 file.getAbsolutePath()
  20.     } catch (Exception e) {
  21.         e.printStackTrace();        
  22.     }

 

如果你要获取图片中的地理位置信息,需要申请ACCESS_MEDIA_LOCATION权限,并使用MediaStore.setRequireOriginal()获取。下面是官方的示例代码:

 

  1. Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
  2.      cursor.getString(idColumnIndex));
  3. final double[] latLong;
  4. // 从ExifInterface类获取位置信息
  5. photoUri = MediaStore.setRequireOriginal(photoUri);
  6. InputStream stream = getContentResolver().openInputStream(photoUri);
  7. if (stream != null) {
  8.     ExifInterface exifInterface = new ExifInterface(stream);
  9.     double[] returnedLatLong = exifInterface.getLatLong();
  10.     // If lat/long is null, fall back to the coordinates (00).
  11.     latLong = returnedLatLong != null ? returnedLatLong : new double[2];
  12.     // Don't reuse the stream associated with the instance of "ExifInterface".
  13.     stream.close();
  14. } else {
  15.     // Failed to load the stream, so return the coordinates (0, 0).
  16.     latLong = new double[2];
  17. }

 

这样下来,一个图片选择器就基本适配完了。

 

补充

 

应用在卸载后,会将App-specific目录下的数据删除,如果在AndroidManifest.xml中声明:android:hasFragileUserData="true"用户可以选择是否保留。

 

对于SAF的使用,可以查看我之前写的SAF使用攻略,这里就不展开说了。

https://weilu.blog.csdn.net/article/details/104199446

 

最后这里有一个介绍Scoped Storage的视频,推荐观看:

https://www.bilibili.com/video/av77198618

 

2、权限变化

 

从6.0开始,基本每次都会有权限方面变动,这次也不例外。

(前几天发布了Android 11的预览版,看来也有权限方面的变化。。。单次权限即将到来)

 

1、在后台运行时访问设备位置信息需要权限

 

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权限表示在前台时拥有访问设备位置信息的权。在请求弹框中,选择“始终允许”表示前后台都可以获取位置信息,选择“仅在应用使用过程中允许”只表示拥有前台的权限。

 

总结一下就是下图:

 

 

其实官方不推荐你使用申请后台访问权的方式,因为这样的结果无非就是多请求一个权限,那么这像变更还有什么意义?申请过多的权限,也会造成用户的反感。所以官方推荐使用前台服务来实现,在前台服务中获取位置信息。

 

1. 首先在清单中对应的service中添加 android:foregroundServiceType="location":

 

  1. <service
  2.     android:name="MyNavigationService"
  3.     android:foregroundServiceType="location" ... >
  4.     ...
  5. </service>

 

2. 启动前台服务前检查是否具有前台的访问权限:

 

  1.     boolean permissionApproved = ActivityCompat.checkSelfPermission(this, 
  2.         Manifest.permission.ACCESS_COARSE_LOCATION== PackageManager.PERMISSION_GRANTED;
  3.     if (permissionApproved) {
  4.        // 启动前台服务
  5.     } else {
  6.        // 请求前台访问位置权限
  7.     }

 

如此一来就可以在Service中获取位置信息。

 

2、一些电话、蓝牙和WLAN的API需要精确位置权限

 

下面列举了Android 10中必须具有 ACCESS_FINE_LOCATION 权限才能使用类和方法:

 

电话

 

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

 

蓝牙

 

  • BluetoothAdapter

    • startDiscovery()

    • startLeScan()

  • BluetoothAdapter.LeScanCallback

  • BluetoothLeScanner

    • startScan()

 

我们可以根据上面提供的具体类和方法,在适配项目中检查是否有使用到并及时处理。

 

3、ACCESS_MEDIA_LOCATION

 

Android 10新增权限,上面有提到,不赘述了。

 

4、PROCESS_OUTGOING_CALLS

 

Android 10上该权限已废弃。

 

3、后台启动 Activity 的限制

 

简单解释就是应用处于后台时,无法启动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"/>

 

  1. Intent fullScreenIntent = new Intent(this, CallActivity.class);
  2. PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, 0,
  3.         fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
  4. NotificationCompat.Builder notificationBuilder =
  5.         new NotificationCompat.Builder(this, CHANNEL_ID)
  6.     .setSmallIcon(R.drawable.notification_icon)
  7.     .setContentTitle("Incoming call")
  8.     .setContentText("(919) 555-1234")
  9.     .setPriority(NotificationCompat.PRIORITY_HIGH) // <--- 高优先级
  10.     .setCategory(NotificationCompat.CATEGORY_CALL)
  11.     // Use a full-screen intent only for the highest-priority alerts where you
  12.     // have an associated activity that you would like to launch after the user
  13.     // interacts with the notification. Alsoif your app targets Android 10
  14.     // or higher, you need to request the USE_FULL_SCREEN_INTENT permission in
  15.     // order for the platform to invoke this notification.
  16.     .setFullScreenIntent(fullScreenPendingIntent, true); // <--- 全屏 intent
  17. Notification incomingCallNotification = notificationBuilder.build();

 

注意:在部分手机上,直接设置setPriority无效(或者说以渠道优先级为准)。所以需要创建通知渠道时将重要性设置为IMPORTANCE_HIGH。

 

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

 

后台启动 Activity 的限制的目的是为了减少对用户操作的中断。如果你有要弹出的页面,推荐你先弹出通知,让用户自己选择接下来的操作,而不是一股脑的强制弹出。(如果你的全屏intent都让用户反感,那他也可以关掉你的通知,不至于任你摆布。)

 

4、深色主题

 

Android 10 新增了一个系统级的深色主题(在系统设置中开启)。虽然深色主题并不是强制适配项,但是它可以带给用户更好的体验:

 

  • 可大幅减少耗电量。OLED 屏幕中每个像素都是自主发光,所以在显示深色元素时像素所消耗的电流更低,尤其在纯黑颜色时像素点可以完全关闭来达到省电的效果。

  • 为弱视以及对强光敏感的用户提高可视性。深色可以降低屏幕的整体视觉亮度,减少对眼睛的视觉压力。

  • 让所有人都可以在光线较暗的环境中更轻松地使用设备。

 

适配方法有两种:

 

1、手动适配(资源替换)

 

官方文档中提到的继承Theme.AppCompat.DayNight 或者 Theme.MaterialComponents.DayNight的方法,但这只是将我们使用的各种View的默认样式进行了适配,并不太适用于实际项目的适配。因为具体的项目中的View都按照设计的风格进行了重定义。

 

其实适配的方法很简单,类似屏幕适配、国际化的操作,并不需要继承上面的主题。比如你要修改颜色,就在res 下新建 values-night目录,创建对应的colors.xml文件。将具体要修改的色值定义在里面。图标之类的也是一个思路,创建对应的 drawable-night目录。

 

只要你之前的代码不是硬编码且代码规范,那么适配起来还是很轻松。

 

2、自动适配(Force Dark)

 

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需要注意几点:

 

  1. 如果使用的是 DayNight 或 Dark Theme 主题,则设置forceDarkAllowed 不生效。

  2. 如果有需要排除适配的部分,可以在对应的View上设置forceDarkAllowed为false。

 

这里说说我实际使用此方法的感受:整体还是不错的,设置的色值会自动取反。但也因此颜色不受控制,能否达到预期效果是个需要注意的问题。追求快速适配可以采取此方案。

 

手动切换主题

 

使用 AppCompatDelegate.setDefaultNightMode(@NightMode int mode)方法,其中参数mode有以下几种:

 

  • 浅色 - MODE_NIGHT_NO

  • 深色 - MODE_NIGHT_YES

  • 由省电模式设置 - MODE_NIGHT_AUTO_BATTERY

  • 系统默认 - MODE_NIGHT_FOLLOW_SYSTEM

 

下面的代码是官方Demo中的使用示例:

 

  1. public class ThemeHelper {
  2.     public static final String LIGHT_MODE = "light";
  3.     public static final String DARK_MODE = "dark";
  4.     public static final String DEFAULT_MODE = "default";
  5.     public static void applyTheme(@NonNull String themePref) {
  6.         switch (themePref) {
  7.             case LIGHT_MODE: {
  8.                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
  9.                 break;
  10.             }
  11.             case DARK_MODE: {
  12.                 AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
  13.                 break;
  14.             }
  15.             default: {
  16.                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  17.                     AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
  18.                 } else {
  19.                     AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY);
  20.                 }
  21.                 break;
  22.             }
  23.         }
  24.     }
  25. }

 

通过AppCompatDelegate.getDefaultNightMode()方法,可以获取到当前的模式,这样便于代码中去适配。

 

监听深色主题是否开启

 

首先在清单文件中给对应的Activity配置 android:configChanges="uiMode":

 

  1. <activity
  2.     android:name=".MyActivity"
  3.     android:configChanges="uiMode" />

 

这样在onConfigurationChanged方法中就可以获取:

 

  1. @Override
  2. public void onConfigurationChanged(@NonNull Configuration newConfig) {
  3.     super.onConfigurationChanged(newConfig);
  4.     int currentNightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
  5.     switch (currentNightMode) {
  6.         case Configuration.UI_MODE_NIGHT_NO:
  7.             // 关闭
  8.             break;
  9.         case Configuration.UI_MODE_NIGHT_YES:
  10.             // 开启
  11.             break;
  12.         default:
  13.             break;    
  14.     }
  15. }

 

详细的内容你可以参看官方文档和官方Demo。

https://developer.android.google.cn/guide/topics/ui/look-and-feel/darktheme

https://github.com/android/user-interface-samples/tree/master/DarkTheme

 

判断深色主题是否开启

 

其实和上面onConfigurationChanged方法同理:

 

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

 

5、标识符和数据

 

对不可重置的设备标识符实施了限制

 

受影响的方法包括:

 

Build

  • getSerial()

 

TelephonyManager

  • getImei()

  • getDeviceId()

  • getMeid()

  • getSimSerialNumber()

  • getSubscriberId()

 

从 Android 10 开始,应用必须具有 READ_PRIVILEGED_PHONE_STATE 特许权限才能正常使用以上这些方法。

 

如果你的应用没有该权限,却仍然使用了以上的方法,则返回的结果会因目标 SDK 版本而异:

 

  • 如果应用以 Android 10 或更高版本为目标平台,则会发生 SecurityException。

  • 如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回 null 或占位符数据(如果应用具有 READ_PHONE_STATE 权限)。否则,会发生 SecurityException。

 

这项改动表示第三方应用无法获取Device ID这类唯一标识。如果你需要唯一标识符,请参阅文档:唯一标识符的最佳做法。

https://developer.android.google.cn/training/articles/user-data-ids

 

当然你也可以试试移动安全联盟(MSA)联合多家厂商共同开发的统一补充设备标识调用SDK。据说还有点不稳定,因为我暂时还没有尝试过,所以不做评价。

http://msalliance.icoc.bz/col.jsp?id=120

 

限制了对剪贴板数据的访问权限

 

除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,否则它无法访问 Android 10 或更高版本平台上的剪贴板数据。

 

对启用和停用 WLAN 实施了限制

 

以 Android 10 或更高版本为目标平台的应用无法启用或停用 WLAN。WifiManager.setWifiEnabled()方法始终返回 false。

 

如果您需要提示用户启用或停用 WLAN,请使用设置面板。

https://developer.android.google.cn/about/versions/10/features#settings-panels

 

6、其他

 

Android10上对折叠屏设备有了更好的支持,对于有折叠屏适配的需求,可以参看为可折叠设备构建应用 和 华为折叠屏应用开发指导。

https://developer.android.google.cn/guide/topics/ui/foldables

https://developer.huawei.com/consumer/cn/doc/90101

 

以上内容只是Android 10中比较大的几项变化,完整的内容可以查看官方文档。

https://developer.android.google.cn/about/versions/10/behavior-changes-all

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/247647
推荐阅读
相关标签
  

闽ICP备14008679号