小米开放平台屏幕圆角适配说明

小米开放平台屏幕圆角适配说明

1.背景

目前大部分小米手机的屏幕都是圆角,如下示意图所示。四个黑色角表示屏幕缺失部分。

2.参数说明

MIUI提供以下两个值分别表示屏幕上下方圆角的半径:

  • rounded_corner_radius_top 
  • rounded_corner_radius_bottom

3.使用方法

例如需要取得rounded_corner_radius_top的值,可以参考如下代码:

public static int getCornerRadiusTop(Context context) {
         int radius = 0;
         int resourceId = context.getResources().getIdentifier("rounded_corner_radius_top", "dimen", "android");
         if (resourceId > 0) {
              radius = context.getResources().getDimensionPixelSize(resourceId);
       
         return radius;
}

小米开发平台全面屏及虚拟键适配说明

小米开发平台全面屏及虚拟键适配说明

1. 前言

自2016年小米 Mix 全面屏手机推出时,得到了业界和用户的双重认可,小米也引领了“全面屏”手机的风潮。作为全面屏手机的引领者,小米将推出更多的全面屏手机,追求更大的屏幕比例,更高的屏占比。

这些变化也影响了手机软件的设计,最值得开发者关注的,是以下两点:

  • 更大的屏幕高宽比
  • 虚拟导航键

2. 更大的屏幕高宽比

大部分全面屏设备都是18:9,从下图可以看到,在 1080P 的分辨率下,比标准的 16:9 屏幕,足足多了240像素。开发者需要作一些优化,以充分利用更大的显示空间。

2.1. 声明 Maximum Aspect Ratio

Android 标准接口中,支持应用声明其支持的最大屏幕高宽比(maximum aspect ratio)。具体声明如下,其中的 ratio_float 被定义为是高除以宽,以 16:9 为例,ratio_float = 16/9 = 1.778 (18:9则为2.0)。

<application>
    <meta-data android:name="android.max_aspect" android:value="ratio_float" />
</application>

若开发者没有声明该属性,ratio_float 的默认值为1.86,小于2.0,因此这类应用在全面屏手机上,默认不会全屏显示,屏幕底部会留黑。考虑到将有更多 19.5:9 甚至更长的手机出现,建议开发者声明 Maximum Aspect Ratio ≥ 2.2 或更多。值得一提的是,如果应用的 android:resizeableActivity 已经设置为 true,就不必设置 Maximum Aspect Ratio 了。详见 Android 官方文档 Declaring maximum aspect ratio

2.2. 避免内容拉伸/变形

从16:9变成18:9甚至更长的比例,图片往往被会拉伸变形,此问题常见于应用开屏图。开发者应使用更灵活的布局,以适应不同的屏幕比例。

2.3. 充分利用屏幕空间

开发者应充分利用全面屏显示更多内容。如下图,王者荣耀已修改了 Maximum Aspect Ratio,在全面屏有更宽阔的游戏视野。

3. 虚拟导航键(Navigation bar)优化

3.1. 虚拟导航键样式

为了实现更高的屏占比,屏幕内的虚拟导航键就成了标准功能,如何让其应用界面在视觉上统一,同样需要开发者的积极适配。Android 已经有相关接口允许开发者自定义虚拟键的样式,以下是可供选择的样式。

关于使用哪种样式,我们有以下建议:

建议1:如果页面含有复杂背景/纹理,建议设置为透明,如下图中的桌面和通话界面。

建议2:含「底部Tab」的页面,建议将虚拟键设置为「底部Tab」的颜色,如 MIUI 的相机和小米商城。

建议3:不含「底部Tab」的页面,建议使用背景颜色,如多看阅读。

由于一个应用内含有多种不同的页面,我们希望开发者能当前页面的情况,来选择合适的虚拟键样式,以保证视觉的统一美观。

3.2. 如何修改虚拟键样式

Android 有标准的实现方式, 调用以下接口即可 window.setNavigationBarColor (int color)。在调用该接口时,还需要设置一些flag,详见该接口的注释说明(即下文):

/**
 * Sets the color of the navigation bar to {@param color}.
 *
 * For this to take effect,
 * the window must be drawing the system bar backgrounds with
 * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
 * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set.
 *
 * If {@param color} is not opaque, consider setting
 * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
 * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
 * <p>
 * The transitionName for the view background will be "android:navigation:background".
 * </p>
 */
public abstract void setNavigationBarColor(@ColorInt int color);​

4. FAQ

4.1. 这些改动是仅针对 MIUI 系统吗

不是。上述提及的均是 Android 标准接口,且早在几年前就已经存在。因此开发者的任何改动,可以在其余 Android 手机中直接生效。我们相信全面屏会是往后手机设计的趋势,做好相关的适配工作,对于开发者来说是非常值得的。

4.2. 如何测试

目前市面上的小米新机均为全面屏手机,测试方法如下:

  • 升级至 MIUI 9.5 及以上版本
  • 前往「设置 > 全面屏 > 应用全屏运行设置」,找到相关应用,然后打开开关,就可以在全面屏比例下运行应用,以观察应用的表现。

有任何问题可以邮件给我们的项目组 miuishell@xiaomi.com ,会有同事解答相关疑问。

小米开发平台剪切板隐私保护功能说明及读写剪切板权限调整说明

小米开发平台剪切板隐私保护功能说明及读写剪切板权限调整说明

1.应用口令规则告知

您的应用若存在通过读取剪切板内容识别并弹出口令的交互逻辑(例如:淘宝读取淘口令),请告知我们您的应用读取的口令规则。

您可将:

  • 开发者名称
  • 应用名称
  • 包名
  • 口令规则(正则patten字符)
  • 口令的使用说明

发送至邮箱miui-security-open@xiaomi.com

我们将在系统中维护您的应用口令,以帮助您的应用能够正确读取剪切板中的必要信息。(注意:我们仅接受在小米应用商店已上架的应用的相关申请)。

2.剪切板权限调整说明

  • 原读写剪切板权限拆分为“读取剪切板”和“写入剪切板”,给予用户更加灵活的隐私权限管控方式;
  • “读取剪切板”权限将默认设置为“智能允许”;“写入剪切板”权限将默认设置为“仅在使用中允许”;
  • 当应用读取剪切板时,若剪切板中的最新内容符合应用读取规则(例如:淘宝读取剪切板中的淘口令),将智能允许应用读取剪切板;智能允许时,将在屏幕顶部通过提示气泡的方式告知用户(用户可见内容为:已智能允许某某应用读取剪切板);
  • 当应用读取剪切板时,若剪切板中的最新内容不符合应用读取规则,且剪切板中的内容符合其他应用读取规则(例如:非淘宝应用读取剪切板中的淘口令),将智能拒绝应用读取剪切板,且用户无感知;
  • 当应用读取剪切板时,若该应用存在读取规则但剪切板最新内容不符合应用读取规则(例如:淘宝读取剪切板中的一段任意文字),将智能拒绝应用读取剪切板,且用户无感知;
  • 当应用读取剪切板时,若剪切板中的最新内容不符合任何应用读取规则且应用不存在读取规则(例如:某地图应用读取剪切板中的一段任意文字),将智能拒绝应用读取剪切板,并弹出通知、气泡告知用户,用户有权利关闭提示。

3.FAQ

3.1.此功能适配哪些Android版本

此功能适配Android的版本:Android 10、 Android 11。

3.2.开发者在哪个MIUI版本可以体验测试

三方开发者可安装MIUI 12开发版20.11.16之后的版本调试。

3.3.智能拒绝的逻辑是否会影响用户主动粘贴剪切板中的内容

不会,只要用户不手动将应用读取剪切板的权限状态手动调整为“拒绝”,用户都可正常手动长按粘贴。此次剪切板隐私保护的功能仅针对应用主动请求读取剪切板的情况。

3.4.读取剪切板权限是否可以使用权限弹窗引导用户授权

读取剪切板权限目前未支持权限询问弹窗,所有应用默认权限状态为“智能允许”。

后台发送本地通知权限管理说明

1.介绍 

安卓系统中,三方应用位于后台运行时会发送大量通知,意图召回用户,严重影响用户体验。该权限可以控制应用在后台运行时发送本地通知的能力。

2.原则

该权限默认拒绝,即应用位于后台时默认不允许发送本地通知。针对特殊应用会提供白名单,例如音乐播放、日程提醒等。白名单应用一旦使用本地通知发送普通消息,或是出现其他有损用户体验等行为时,将永久取消白名单。普通消息定义请参考:https://dev.mi.com/console/doc/detail?pId=2086

3.建议

开发者可以接入小米推送,使用push消息触达并服务用户,相关接入流程请参考:https://dev.mi.com/console/doc/detail?pId=230

小米开发平台后台弹出页面权限管理说明

小米开发平台后台弹出页面权限管理说明

1. 介绍 

安卓系统中,由于三方应用可以随意从后台弹出页面,严重影响用户体验,该权限可以控制应用是否可以在后台启动页面。

2. 原则

该权限默认为拒绝的,既为应用默认不允许在后台弹出页面,针对特殊应用会提供白名单,例如音乐(歌词显示)、运动、VOIP(来电)等;

白名单应用一旦出现推广等恶意行为,将永久取消白名单。

小米手机设备管理器权限管理说明

小米手机设备管理器权限管理说明

1. 介绍 

设备管理器权限是Android提供给(企业)设备管理类应用的设备保护功能,对手机设备进行管理和操作的接口权限。

权限接口涉及对用户数据、密码的操作,安全性风险极高。

部分应用滥用权限声明来进行防卸载保护,应用内并无相关功能,当用户开启后就无法卸载。

2. 原则 

  • 设备管理器是提供给设备管理类应用的系统权限接口,禁止应用滥用设备管理器权限提供非设备管理功能外的其他功能及服务,包括但不限于清除用户数据、防卸载等。
  • 对于引导或提供非正常使用设备管理器权限的应用,按标准执行系统管控策略。包括但不限于:在系统内强提醒用户进行关闭处理、禁止应用获取相关服务或权限接口。
  • 对于引导或提供通过设备管理器权限,对用户的数据、设备使用安全可能产生危害的应用,将严格执行:将该应用在小米应用商店进行下架处理、禁止应用获取相关服务接口 、禁止相关应用在设备管理器应用列表中显示。
  • 禁止一揽子权限授权原则,各APP只能在其核心业务功能需要特定权限,且用户不同意授予该权限时,才允许退出应用。否则,非核心功能的权限调用中,应用不得以用户不授权而强制退出应用,相关权限应当为设计时的默计关闭保护状态。

小米手机默认桌面应用管理说明

小米手机默认桌面应用管理说明

1. 介绍 

安卓系统中,由于第三方桌面类的应用不稳定,会带来手机系统卡顿、手机功耗大、偷偷下载应用、恶意扣费等问题,给用户带来了很大的使用困扰。

2. 原则

桌面作为系统常用、基础并承担着入口安全责任的重要应用,为保证系统的稳定、完整、一致性,小米方强制禁止三方应用设为默认桌面,只允许使用系统桌面。

小米开发平台隐身模式三方应用适配文档

小米开发平台隐身模式三方应用适配文档

说明:若不涉及到使用录音、相机、麦克风权限,则可忽略此项适配

1.MIUI「隐身模式」功能

功能效果:

  • 用户开启该模式后,应用将无法使用ACCESS_FINE_LOCATION、CAMERA、RECORD_AUDIO三项权限
  • 应用请求对应权限时,系统会弹出通知告知用户“隐身模式已开启,应用无法xx”,但为避免频繁打扰用户,该通知有弹出策略,所以并不会每次应用使用权限时都弹出,如下图:

2.需适配的场景:

若三方应用在隐私模式开启状态下,因无法使用定位、相机、麦克风而无任何提示,且用户忽略了系统弹出push,可能使用户产生疑惑,认为应用出现问题,影响用户体验

适配方案:

MIUI提供两项属性值以供业务查询当前隐私模式的开启状态,便于在用户开启隐身模式时弹出“因开启隐身模式故无法使用麦克风相机获取定位”解释文案的弹窗

判断隐身模式为开启状态的属性值:

1. public static final String KEY_INVISIBLE_MODE_STATE = “key_invisible_mode_state”;

Settings.Secure.getInt(getContentResolver(), PermTipsUtils.KEY_INVISIBLE_MODE_STATE, 0) == 1;

2. public static final String KEY_INVISIBLE_MODE_PROP = “persist.sys.invisible_mode”;

SystemPropertiesUtils.get(PermTipsUtils.KEY_INVISIBLE_MODE_PROP) 是 “1”

3.功能体验说明:

若想体验该功能,机型和版本要求如下:

机型:小米10、小米9、红米Redmi K40游戏增强版、红米Redmi K30至尊纪念版、红米Redmi 9、MIX FOLD

MIUI版本:21.6.5之后的开发版rom 

小米开放平台MIUI进程管理适配说明

小米开放平台MIUI进程管理适配说明

1. 介绍

应用进程的存活与否常常受到三方开发者的关注。与原生系统不同,MIUI在Android系统的基础之上,开发了一套进程管理模块,便于系统管理运行中的进程。

此文档将会提供进程相关信息,方便开发者适配MIUI进程管理机制、初步自查应用被杀原因、更精确的向我们的三方团队同事反馈应用异常被杀问题

2. 进程管理功能

MIUI的进程管理功能大致分为两类:用户主动触发、用户被动触发

用户主动触发的功能包含:

 名称 触发入口Reason 
 一键清理 最近任务/悬浮球 OneKeyClean
 强力清理 负一屏 ForceClean
 垃圾清理 安全中心  GarbageClean
  锁屏清理 安全中心LockScreenClean
 游戏清理 安全中心 GameClean
 优化清理 安全中心 OptimizationClean
上滑清理 最近任务SwipeUpClean 

如果发现应用因为以上原因被杀死,那么意味着是用户在触发入口主动杀死这些应用

用户被动触发的功能包含:

 名称 被动触发场景 Reason
 Power异常查杀 应用过度耗电 AutoPowerKill
 Thermal异常查杀 应用使手机发热 AutoThermalKill

如果发现应用因为以上原因被杀死,那么意味着应用出现异常,会影响到系统正常运作,系统将应用清理掉了

3. FAQ

3.1. 我的应用在原生上运行时正常,但在MIUI上频繁被杀,该怎么定位原因

可以在shell中输入命令:

adb logcat -b events | grep am_kill

查看被杀应用的日志,例如:

1494  2963 I am_kill : [0,5253,com.eg.android.AlipayGphone,500,LockScreenClean]

最后一列信息即为被杀的Reason,和上面的表格进行一下对比,即可定位被杀的原因

3.2. 我的应用频繁的被AutoPowerKill/AutoThermalKill杀死,该怎么解决这个问题

当出现应用频繁被被动原因杀时的情况时,开发者首先应该自己检查下自身应用有没有过度耗电、发热的行为

如果确认自身应用质量没有问题,可以打个bugreport并联系我们的三方团队的同事进行反馈,我们内部的对应开发会进行深度分析

3.3. 我的应用被用户主动杀死后,怎么样可以让应用进程自动重新启动呢

开发者可以在用户使用应用时,引导用户在安全中心中打开自启动开关

小米耳返功能SDK适配说明

1、耳返功能sdk简介

小米手机目前高通平台机型上提供K歌低延时耳返功能 ,用户可以在小米手机上体验震撼的K歌效果,诚邀应用开发者适配,感谢支持!

支持耳返功能的设备

itgsa接口小米12S, 小米12S Pro,小米12S Ultra和其他出厂系统为Android 13版本的高通平台机型
小米接口其他高通平台机型

2、sdk接入方法

2.1 权限说明

需要应用权限配置:

android.permission.MODIFY_AUDIO_SETTINGS

android.permission.RECORD_AUDIO

2.2 API使用说明

1)小米12S, 小米12S Pro,小米12S Ultra和其他出厂系统为Android 13版本的高通平台机型获取MediaClient单例,其他高通平台机型获取KaraokeMediaHelper单例

2)isSupported判断应用是否支持KTV功能使用,小米平台通过应用白名单控制是否支持app使用KTV功能。 若app申请支持KTV功能,请联系我们(gengping@xiaomi.com

3)演唱开始,先打开KTV系统,openKTVDevice

4)设置相关配置:

setMixerSoundType 混响音效类型

setEqualizerType EQ音效类型

setPlayFeedbackParam 耳返开关

setMicVolParam 耳返音量大小

5)再开启播放,最后开启录音 【小米仅支持deep buffer播放方式的KTV效果】

6)演唱结束,先关闭播放、录音

7)再closeKTVDevice 关闭KTV系统

2.3 接口函数列表

  • itgsa结构机型

小米12S、小米12S Pro、小米12S Ultra和其他出厂系统为Android 13版本的高通平台机型

详情请参考 DEMO

接入实例参考 com.example.mediademo

函数名称功能简介
initialize初始化并获取KTV MediaClient单例。
getVersion获取KTV SDK库版本号。
isDeviceSupportKaraoke判断当前机器设备能否支持KTV。
isAppSupportKaraoke应用是否支持KTV。【oppo、vivo返回默认值true,小米手机检测】补充说明:小米平台通过应用白名单控制是否支持app使用KTV功能。 若app申请支持KTV功能,请联系我们(gengping@xiaomi.com),邮件说明应用包名和应用功能简介。
isSupported应用是否支持KTV功能使用,注意此为前三个接口组合判断结果,通常来说三方只使用该接口判断是否支持。
getKaraokeSupportParameters应用获取当前机器支持KTV的JSON参数信息,比如应用设置何种参数(AudioTrack的采样率、flag等,AudioRecord的source等),判断是否可以正常使用KTV功能。
openKTVDevice打开KTV设备,此动作必须是刚发生在播放伴奏前。
closeKTVDevice关闭KTV设备。
setPlayFeedbackParam控制耳返开启/关闭接口,系统默认打开,建议无论怎么app调用都打开调用一次,排除其他app不正确调用的干扰。
getPlayFeedbackParam获取当前耳返开关状态。
setMicVolParam设置人声音量大小。
getMicVolParam获取当前人声音量值。
setMixerSoundType设置混响效果。 ( 0:无、1:KTV、2:剧场、3:音乐厅、4:录音棚 )
setEqualizerType设置EQ均衡器音效。 (0:无、1:标准、2:浑厚、3:清脆、4:明亮)
getExtMixerSoundType扩展混响音效。
getExtEqualizerType扩展EQ均衡器音效。
  • 小米结构机型

详情请参考:KaraokeMediaHelper

接入实例参考 com.miui.media.KaraokeMediaHelper

函数名称功能简介
KaraokeMediaHelper初始化KTV工具类
isDeviceSupportKaraoke判断当前机器设备能否支持KTV。
getKaraokeSupportParameters应用获取当前机器支持KTV的JSON参数信息,比如应用设置何种参数(AudioTrack的采样率、flag等,AudioRecord的source等),判断是否可以正常使用KTV功能
isDeviceSupportMixerSound判断当前机器设备能否支持KTV。
openKTVDevice打开KTV设备,此动作必须是刚发生在播放伴奏前 。
closeKTVDevice关闭KTV设备。
isAppSupportKaraoke应用是否支持KTV。【小米手机检测】补充说明:小米平台通过应用白名单控制是否支持app使用KTV功能。 若app申请支持KTV功能,请联系我们(gengping@xiaomi.com),邮件说明应用包名和 0:无、1:KTV、2:剧场应用功能简介。
setMixerSoundType设置混响效果。( 0:无、1:KTV、2:剧场、3:音乐厅)
setPlayFeedbackParam控制耳返开启/关闭接口,系统默认打开,建议无论怎么app调用都打开调用一次,排除其他app不正确调用的干扰。
setMicVolParam设置人声音量大小。
getPlayFeedbackParam获取当前耳返开关状态。
getMicVolParam获取当前人声音量值。
setEqualizerType设置EQ均衡器音效。(0:无、1:标准、2:浑厚)
getExtMixerSoundType扩展混响音效。
getExtEqualizerType扩展EQ均衡器音效。

小米跑步机传感器数据集成到计步器数据库说明

1.小米跑步机传感器简介

当手机放置在跑步机上时,收集手机中传感器的数据,判断是否在跑步机上运动,若运动一步则跑步机传感器上报一次数据1,不运动则不报。

2.将跑步机数据集成到计步器数据库

2.1.通知系统服务

在注册或者解除注册跑步机传感器的时候通过Binder告知系统服务。 这步骤为必须操作,否则跑步机计步器数据无法同步至系统计步数据库。

代码示例:

public class MainActivity extends AppCompatActivity {

    private SensorManager mSensorManager;
    private Sensor mTreadmillSensor;
    private TreadmillListener mTreadmillListener;
    private static final int TREADMILL_SENSOR = 33171041;
    private static final String SERVICE_NAME = "miui_step_counter_service";
    private Binder mBinder;

    @RequiresApi(api = Build.VERSION_CODES.Q)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        mTreadmillSensor = mSensorManager.getDefaultSensor(TREADMILL_SENSOR,true);
        mTreadmillListener = new TreadmillListener();
        //监听跑步机Sensor
        mSensorManager.registerListener(mTreadmillListener,mTreadmillSensor,mSensorManager.SENSOR_DELAY_NORMAL);
        //通知系统服务,注册的时候发送true
        sendMessage(true);
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    public void sendMessage(boolean is){
        //获取系统服务,ServiceManager报错底下有解决方案
        IBinder binder = ServiceManager.getService(SERVICE_NAME);        
        if (mBinder == null) {
            mBinder = new Binder();
        }
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        data.writeInterfaceToken("miui_step_counter_service");
        data.writeBoolean(is);
        //传过去一个全局Binder(为了感知本类是否被销毁)
        data.writeStrongBinder(mBinder);
        try {
            binder.transact(0,data,reply,0);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private class TreadmillListener implements SensorEventListener{

        @Override
        public void onSensorChanged(SensorEvent sensorEvent) {

        }

        @Override
        public void onAccuracyChanged(Sensor sensor, int i) {

        }
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    @Override
    protected void onDestroy() {
        super.onDestroy();
        //解注册的时候,通知系统服务,发送false
        mSensorManager.unregisterListener(mTreadmillListener,mTreadmillSensor);
        sendMessage(false);
    }
}

2.2.ServiceManager拿不到解决方案

在代码中创建一个包名为android.os,类名为ServiceManager的类。

代码示例:

package android.os;

public class ServiceManager {
    private ServiceManager(){}

    public static IBinder getService(String name){
        return null;
    }
}

2.3.说明

目前跑步机计步传感器支持小米12、小米12 Pro、小米12S Pro、小米12S Ultra四款机型,如果您的应用在注册下面这个特定传感器type时返回的sensor对象为空,说明当前机型不支持跑步机计步传感器,应用可通过此sensor对象的返回值来判定该功能是否生效。

  mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        mTreadmillSensor = mSensorManager.getDefaultSensor(TREADMILL_SENSOR,true);
        //根据mTreadmillListener是否为空来判定当前手机是否支持跑步机计步的功能
        mTreadmillListener = new TreadmillListener(); 

小米开发平台小米计步器接口适配说明

小米开发平台小米计步器接口适配说明

1.小米计步器简介 

收集手机中传感器的数据,通过机器学习算法判断步数。 

1.1 算法说明 

  • 计步器Sensor 5分钟上报一次数据,后台Service将计步数据记录插入本地数据库,若没有数据上报, 则不插入; 
  • app第一次请求数据会立即响应, 后台Service会立即返回给app最新的记录数据;
  • app在1分钟内多次请求数据, 则只有第一次得到的数据是最新的, 后面的请求结果和第一次相同; 
  • 一条记录只有一种计步模式.。例如, 用户在10分钟内有600步数据, 400步走路, 200步跑步, 则这10分钟会分拆成两条记录, 400步走路和200步跑步;
  • 只传给应用层3种计步模式: 0: 不支持(在不支持计步的手机上不会得到数据), 2: 走路, 3: 跑步。 

2.计步器接入 

2.1 判断本机是否支持计步 

使用miui.util.FeatureParser提供的接口去判断是否支持stepsProvider功能

//示例code
//在项目中新建一个工具类,FeatureParser,通过反射机制来获取miui.util.FeatureParser
public class FeatureParser {
    public static boolean getBoolean(String name, boolean defaultValue) {
        try {
            Class featureParserClass = Class.forName("miui.util.FeatureParser");
            Method method = featureParserClass.getMethod("getBoolean", String.class, boolean.class);
            return (Boolean) method.invoke(null, name, defaultValue);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return defaultValue;
    }
}

//在功能开始之前判断是否支持stepsProvider功能
boolean isSupport= FeatureParser.getBoolean("support_steps_provider",false); 

2.2 App采用ContentProvider的query请求获取计步数据 

接口格式: 

Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
参数必选类型范围说明
uritrueandroid.net.UriUri.parse(“content://” + “com.miui.providers.steps” + “/item”);取固定值
projectionfalseString[]可选列: “_id”, “_begin_time”, “_end_time”, “_mode”,  “_steps”获取指定列, 若为null则获取所有列
selectionfalseString从可选列中选取自定义条件获取满足条件的记录行, 若为null则获取所有行
selectionArgsfalseString[]一般为可选列的值Selection中带?的格式化参数
sortOrderfalseString从可选列中选取列升序(asc)或降序(desc), 可多个并列返回结果的记录行排序方式, 若为null则按id大小排序

2.3 返回结果: 满足查询条件的记录行

记录行的数据结构如下 

public class Step {
    private int mId; // 记录在sqlite的id
    private long mBeginTime; // 计步开始时间
    private long mEndTime; // 计步结束时间
    private int mMode; // 计步模式: 0:不支持模式, 1:静止, 2:走路, 3:跑步, 11:骑车, 12:交通工具 
    private int mSteps; // 总步数
}

3.代码示例

3.1 AndroidManifest.xml中声明权限 

 <uses-permission android:name=”miui.permission.READ_STEPS” />(不声明权限无法读取计步数据)。 

3.2 Query用到的数据结构 

public class Steps {
/* Data Field */
public static final String ID = "_id";
public static final String BEGIN_TIME = "_begin_time";
public static final String END_TIME = "_end_time";
// 0: NOT SUPPORT 1:REST 2:WALKING 3:RUNNING
public static final String MODE = "_mode";
public static final String STEPS = "_steps";
/* Default sort order */
public static final String DEFAULT_SORT_ORDER = "_id asc";
/* Authority */
public static final String AUTHORITY = "com.miui.providers.steps";
/* Content URI */
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/item");
}
public static String[] projection = new String[] {
Steps.ID,
Steps.BEGIN_TIME,
Steps.END_TIME,
Steps.MODE,
Steps.STEPS
};

3.3 返回结果的数据结构 

public class Step {
private int id;
private long mBeginTime;
private long mEndTime;
private int mMode;
private int mSteps;
}

3.4 查询方法示例 

public LinkedList<Step> getAllSteps(String selection, String[] args) {
LinkedList<Step> steps = new LinkedList<Step>();
Cursor cursor = resolver.query(Steps.CONTENT_URI, projection, selection, args,
Steps.DEFAULT_SORT_ORDER);
if (cursor.moveToFirst()) {
do {
Step s = new Step(cursor.getInt(0), cursor.getLong(1), cursor.getLong(2),
cursor.getInt(3),
cursor.getInt(4));
steps.add(s);
} while (cursor.moveToNext());
}
return steps;
}

小米开发平台小米妙播适配说明

小米开发平台小米妙播适配说明

一、小米妙播背景与介绍

1.1 安卓原生Media Session介绍

Android 框架定义了两个类(媒体会话和媒体控制器),它们为构建媒体播放器应用提供了一个完善的结构。

媒体会话和媒体控制器通过以下方式相互通信:使用与标准播放器操作(播放、暂停、停止等)相对应的预定义回调,以及用于定义应用独有的特殊行为的可扩展自定义调用。

更多信息详见:https://developer.android.com/guide/topics/media-apps/media-apps-overview

1.2 MIUI小米妙播介绍

小米妙播支持基于Media Session的音频播控、基于Wi-Fi的跨设备音乐接力。

用户可以在小米手机和平板的系统控制中心、通知栏/锁屏媒体中心使用该功能。

控制中心内的小米妙播播控,基于安卓原生Media Session能力实现,音频应用适配Media Session即可,众多知名三方应用均已适配。

二、适配小米妙播的好处

2.1 系统全局常驻播控

小米妙播作为控制中心常驻的播控、互联入口,用户可以在手机全局操作音频,是Android厂商中全面媲美苹果 AirPlay2 的系统级功能。

2.2 覆盖更多场景的播放

各音频app的用户活跃场景不再局限于手机,用户用音箱等设备听歌时,也可使用各音频app,通过小米妙播互联播放。

三、适配方式

基本适配方式请参考谷歌官方文档:

https://developer.android.com/guide/topics/media-apps/working-with-a-media-session

https://developer.android.com/guide/topics/media-apps/audio-app/building-a-mediabrowserservice

适配要点:

及时更新Meta信息

// 构建MediaSession
MediaSessionCompat mMediaSession;
//构建MediaMetadata,并传入媒体meta信息(歌曲名、专辑名、歌手名、歌曲时长等)
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder();
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, getTrackName())
        .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, getAlbumName())
        .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, getArtistName())
        .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration());
MediaMetadataCompat metadata = builder.build();
mMediaSession.setMetadata(metadata);
// 构建并传入歌曲封面
Bitmap result;
// 获取封面BitMap...
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, result);
mMediaSession.setMetadata(builder.build());

及时更新播放状态信息

// 构建PlaybackState,传入播放状态、播放进度等信息。
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(getSourcePlaybackState())
        .setBufferedPosition(getBufferedPosition())
        .setState(PlaybackStateCompat.STATE_PLAYING, position(), getSpeedRatio());
mMediaSession.setPlaybackState(builder.build());

处理播放回调 

// 构建MediaSessionCompat.Callback回调,处理播放、暂停、上一首、下一首、调整播放进度等操作
private class MediaPlaybackSessionCallback extends MediaSessionCompat.Callback {
    @Override
    public void onPlay() {
        play();
    }
    @Override
    public void onPause() {
        pause();
    }
    @Override
    public void onSkipToNext() {
        next();
    }
    @Override
    public void onSkipToPrevious() {
        prev();
    }
    @Override
    public void onFastForward() {
        forward();
    }
    @Override
    public void onRewind() {
        backward();
    }
    @Override
    public void onStop() {
        stop();
    }
    @Override
    public void onSeekTo(long pos) {
        seek(pos);
    }
}
mMediaSession.setCallback(new MediaPlaybackSessionCallback());

小米开发平台MiHaptic适配说明

小米开发平台MiHaptic适配说明

1.MiHaptic简介

MiHaptic是一套振动波形的生成框架,可以通过参数或者HE格式为你的线性马达机器定制出丰富的振动效果。

1.1.能力

MiHaptic支持接入不同的触感波形生成算法以驱动手机上的线性马达,目前支持小米自研算法和RichTap。

1.2.场景及案例

  • QQ音乐节奏实验室

QQ音乐节奏实验室4D振感,开启音乐随振,让听歌更有节奏感。

  • 和平精英高品质振动

和平精英高品质振动使用MiHaptic方案对不同枪械、载具、脚步声、玻璃破碎等场景提供拟真的触感反馈。

2.使用入门

2.1.开发环境

  • 安卓Studio 4.0 以上

2.2.开发准备

2.2.1.集成MiHaptic SDK

  • miui.os

DynamicEffect_0807_2

提供组合PrimitiveEffect的方式。

  • android.os

DynamicEffect_0128

提供HE文件振动方式。

2.2.2.增加振动权限

2.2.3.使用 MiHaptic SDK文档 中的接口。

DynamicEffect_ZH.docx

2.3.使用MiHaptic

  • 组合PrimitiveEffect的方式

DynamicEffect可组合Transient和Continuous类型的效果,如下图所示

DynamicEffect 数据结构实际上是对一组参数的描述,一个DynamicEffect由若干个PrimitiveEffect和若干Parameter所组成。

PrimitiveEffect 分为两种,一种是描述瞬时振动的Transient,例如哒哒哒的清脆效果;另一种为Continuous,这种振动效果的特点是持续时间长,例如嗡嗡嗡的效果。

对于每种PrimitiveEffect,有两个重要的参数,intensity和sharpness,分别描述它们的强度与锐度。

Parameter与PrimitiveEffect的两个重要属性对应,用来控制PrimitiveEffect的强度或者锐度。

Parameter 可以添加到整个DynamicEffect里,那么它可以控制所有时间范围内的PrimitiveEffect。

Parameter 也可以添加在Continuous效果中,用来作渐变。

例如,下图所示的是一个由两个Continuous与一个Transient所组合成的效果:

DynamicEffect effect = DynamicEffect.startCompose(); 
 
DynamicEffect.PrimitiveEffect continuous1 = DynamicEffect.createContinuous(0.5, 1.0, 3.0); 
DynamicEffect.Parameter param1 = DynamicEffect.createParameter(DynamicEffect.INTENSITY,  
                                                                  new float[]{0.5, 1, 2} , new float[] {0.6 , 0.3, 0.2); 
continuous1.addParameter(1, param1); 
 
DynamicEffect.PrimitiveEffect transient = DynamicEffect.createTransient(0.8, 1.0); 
 
DynamicEffect.PrimitiveEffect continuous2 = DynamicEffect.createContinuous(0.3, 1.0, 6.0); 
DynamicEffect.Parameter param2 = DynamicEffect.createParameter(DynamicEffect.INTENSITY,  
                                                                  new float[]{1.0, 2.0} , new float[] {0.3, 1.0); 
continuous1.addParameter(2, param2); 
 
effect.addPrimitive(0, continuous1); 
effect.addPrimitive(3.5, transient); 
effect.addPrimitive(4, continuous2); 
 
DynamicEffect.Parameter global = DynamicEffect.createParameter(DynamicEffect.INTENSITY,  
                                                                  new float[]{0, 8.0} , new float[] {0.2, 1.0); 
 
effect.addParameter(2.5); 

HapticPlayer player = new HapticPlayer(effect);
player.start();

上图描述了一个DynamicEffect以及intensity属性的渐变示意图。DynamicEffect中包含了两个Continuous和一个Transient。

生成波形的强度在0时刻的强度为0.5,它是由第一个Continuous创建时所指定的。 在第0.5S时强度为0.6,该变化是由Continuous被自己的Parameter所控制引起。第2.5S时,Continuous的强度渐变到了0.04,该变化由于Global参数对其产生了影响,对于强度而言,global参数与PrimtiveEffect参数的强度作乘积,因此变为0.2*0.2=0.04 。而后该Continuous的Intensity无自身Parameter影响,但由于globalParameter存在,从2.5到第3S改变到0.05。

在第3.5S时开始播放Transient,强度为初始值0.8与当前时刻的global参数0.3相乘为0.24。(global起始时间为2.5S,因此在3.5S时参数的值渐变为0.3)。

第二个Continusou起始时间为第4S,从第4s-第6S强度递增,因为当前依旧属于global参数的作用时间,第4S强度为=0.3*0.35 = 0.105,后面的计算方式同理。

  • 使用HE文件
{
 "Metadata": {
 "Version": 1, // 版本号,整形
 "Created": "2020-07-08", // 创建时间,String类型
 "Description": "game haptic" // 震动效果描述,String类型
 },
 "Pattern": 
 [
 {
 "Event": {
 "Type": "continuous", // 事件类型: continuous->持续震动。transient->简短震动
 "RelativeTime": 0, // 相对开始时间, 整形, 单位ms
 "Duration": 300, // 持续震动类型参数:持续时间。整形, 单位ms
 "Parameters": {
 "Intensity": 80, // 震动强度, 整形, [0,100]。0->平台支持的最小值, 100->平台支持的最大值。
 "Frequency": 50, // 震动频率, 整形, [0,100]。0->平台支持的最小值, 100->平台支持的最大值。
 "Curve": [ // 持续震动类型参数:曲线。实现上保证平滑过渡效果
 {"Time": 0, "Intensity": 0, "Frequency": 25}, // 起始点,必须。time为RelativeTime,Intensity必须取值为0。
 {"Time": 100, "Intensity": 0.7, "Frequency": -30}, 
 {"Time": 200, "Intensity": 0.75, "Frequency": 90},
 {"Time": 300, "Intensity": 0, "Frequency": 50} // 结束点,必须。time为Duration,Intensity必须取值为0。
 ]
 }
 }
 },
 {
 "Event": {
 "Type": "transient", // 事件类型: continuous->持续震动, transient->简短震动
 "RelativeTime": 400, // 相对开始时间, 整形, 单位ms
 "Parameters": {
 "Intensity": 80, // 震动强度, 整形, [0,100]。0->平台支持的最小值, 100->平台支持的最大值。
 "Frequency": 40 // 震动频率, 整形, [0,100]。0->平台支持的最小值, 100->平台支持的最大值。
 }
 }
 }
 ]
}

HE文件描述如上面的JSON格式所示,可以将描述文件放在项目中,使用DynamicEffect.create(string)接口创建效果。

InputStream is = getResources().openRawResource(R.raw.demo_he);
try {
    int size = is.available();
    byte[] buffer = new byte[size];
    is.read(buffer);
    is.close();
    str = new String(buffer);
} catch (Exception e){}
effect = DynamicEffect.create(str);
player = new HapticPlayer(effect);
player.start();

注:使用HE文件创建出的DynamicEffect不可再用于添加PrimitiveEffect,HE文件当前仅支持16个以内的event。

小米开放平台三方App相机无法对焦适配方案

小米开放平台三方App相机无法对焦适配方案

1.问题现象

打开App扫一扫 -> 画面一直无法聚焦

2.问题分析

对焦失败的原因:第一次AFtrigger被reconfigure冲掉了,导致AF模块状态异常,一直无法对焦。

2.1.App的对焦方式

扫一扫采用auto方式对焦,即先设置AFMode=auto,然后周期性不断下发aftrigger(loop focus)来主动触发每一次对焦。原理如下:

                                                       图1 auto对焦原理图

2.2.configure_stream和reconfigure

  • 什么是configure_stream

configure_stream是Cameraservice的行为。在open camera初始化阶段,Cameraservice会根据app的对分辨率、画幅、stream等需求向底层发送configure_stream的指令。通俗的讲,比如某个camera需要预览和yuv数据,那么在初始化阶段就需要将这些需求通知底层camera,Camera会根据这些需求搭建对应的硬件和软件资源,当底层准备完毕后,就可以开始接收app的request需求,并返回数据。

  • 什么是reconfigure

当Cameraservice检测到app的需求发生变化时,便会触发reconfigure,将新的需求向底层发送configure_stream的指令。例如:一开始app只需要预览,那么Cameraservice就会向底层configure预览流,底层就会搭建好预览流的输出通道,过了几帧后,app又需要yuv了,于是就会执行reconfigure,此时会将上次configure的资源全部清除,并重新建立预览+yuv的通道。

2.3.微博的流程

以微博扫一扫为例分析不对焦的原因,不同app行为可能有所不同。

                                                   图2 微博扫一扫对焦流程图

一般对于扫码的场景,app会请求预览流(用于显示)和yuv流(用于处理、识别二维码),但由于yuv并不需要马上使用,因此,预览和yuv的请求可能会存在时间差。

  • 为什么旧的机型这些app没有问题,在新平台上会有问题

根据以上分析可知,不对焦有两个条件,一是触发了reconfigure,二是reconfigure冲掉了第一次AFtrigger。因此对于很多app,reconfigure的问题一直存在,但reconfigure的时机具有随机性,如果第二次configure发生在第一次AFtrigger之前,就不会冲掉trigger,也就不会引起问题。

3.修复方案

通过与微博研发人员合作验证以下两种方案均可修复该问题:

  • 方案1:将第一次触发AF的时机移到get yuv之后;
  • 方案2:同时请求预览和yuv,避免时间差导致的reconfigure。

建议方案2,可以根源上解决问题。如下是微博的修复方案(不同app可能情况不一样,仅供参考):在startpreview()前执行setPreviewCallback(YUV)。

说明:在开启预览时app会调用startpreview()接口,如果在此之前调用setPreview Callback(),提前设置yuv的callback,这样便可以在预览时同时请求yuv流。否则,startpreview()只会请求预览流,当执行到setPreviewCallback(YUV),会触发reconfigure请求yuv。

小米开发平台MIUI无极音量适配说明

小米开发平台MIUI无极音量适配说明

1.MIUI无极音量综述

MIUI为了提升用户的音频体验,将媒体音音量范围从[0,15]修改为[0,150]。用户在滑动音量条时,每一次轻微的滑动都能改变音量,细微的音量调节让用户能找到更加合适的音频响度。

2.App适配建议

2.1.若App无调节音量功能或不拦截音量上下按键,此时无需单独适配

2.2.若App通过onTouchEvent等接口拦截volume up或down事件

  • 拦截事件后,做其他的事件处理,例如按键拍照等,此时App无需单独适配;
  • 拦截事件后,通过AudioManager的adjustSuggestedStreamVolume接口调节音量,此时MIUI 系统会内部计算每次按键应该调节多少index,此时App无需单独适配;
  • 拦截事件后,通过AudioManager的setStreamVolume接口,此时接口要传递具体的index值,此时App需适配。

App应内部自己定义步长step,进而算出index。

若支持无极音量step = 10,若不支持step = 1;原因是为了兼容不支持无极音量的机型,统一定义:按键15次将音量调满。

统一计算公式  step = MaxVolume /15;

MaxVolume = AudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)。

总结:简单来说,应用App如果有自己的音量调节逻辑,应该内部定义步长step。通过以上方式可兼容任意机型。       

3.未适配的应用具体表现

  • 具体表现为:按键150次,才能调满音量,调节缓慢。

4.联系我们

MIUI 期待您的适配,若有任何问题,请及时与我们沟通:liuxiaoyu7@xiaomi.com。

小米开放平台双WiFi适配说明

小米开放平台双WiFi适配说明

1.双WIFI简介

双WiFi顾名思义提供了手机同时连接两个WiFi同时上网的能力,一般手机只能同时连接AP的一个WiFi频段获取 WiFi信号,而支持双WiFi技术的手机可以同时连接AP的两个频段(2.4Ghz或5GHz)获取WiFi信号(可以连接一个AP的两个频段,也可以连接两个AP的不同频段)。

1.1双WIFI的优点

  • 提升网络稳定性:如果一路链路因为无线参数波动出现网络波动,可以通过另一链路及时将数据包发送出去;
  • 提升网络速度:双WiFi提供了两条链路同时加载数据的能力,提升网络速度。

1.2目前小米支持双WiFi的已发布机型

小米10、小米10 Pro、Redmi K30 5G、Redmi K30 Pro。

1.3如何进入双WIFI UI操作界面

双WiFi UI操作界面可从手机如下操作路径:“设置->WLAN->WLAN助理->双WLAN加速”进入。该界面称为副WiFi界面,副WiFi界面的打开和连接与主WiFi除了频段差异外并无区别(当主WiFi连接5Ghz时,副WiFi只能连接2.4Ghz,反之主WiFi连接2.4Ghz的时候,副WiFi只能连接5Ghz)。如下图所示:

2.双WIFI接入

2.1双WIFI接入需求

  • 判断当前机型是否支持双WiFi;
  • 判断当前双WiFi是否已连接;
  • 若未连接,作双WiFi的连接引导;
  • 接入双WiFi 网络;
  • 副WiFi相关的api。

2.2判断当前机型是否支持双WiFi

private boolean supportDualWifi(Context context) {
    String cloudvalue = Settings.System.getString(context.getContentResolver(), "cloud_slave_wifi_support");
    if ("off".equals(cloudvalue)) {
        return false;
    }
    boolean support;
    try {
        support = context.getResources().getBoolean(
                context.getResources().getIdentifier("config_slave_wifi_support", "bool", "android.miui"));
    } catch (Exception exception) {
        return false;
    }
    return support;
}

2.3判断当前双WIFI是否已连接

WiFi的api都属于系统api,有严格的权限校验,第三方应用无法直接调用。miui提供了workround判断方法,当副WiFi连接上了之后,如下参数会设置为对应的副WiFi的ssid,断开时则会清空,可依此判断当前副WiFi是否已连接。

 private boolean isSlaveWifiConnected(Context context) {
        String slaveWifiSsid =  getDualWifiSsid(context);
        if (slaveWifiSsid == null || slaveWifiSsid.isEmpty()){
            return false;
        } else {
            return true;
        }
    }


    private String getDualWifiSsid(Context context){
        return Settings.System.getString(context.getContentResolver(), "slave_wifi_ssid");
    }

2.4 若副WiFi未连接,则引导用户至副WiFi界面进行连接

判断当前副WiFi是否已连接,若未连接跳转到系统副WiFi界面引导用户连接,返回到当前界面则再次校验,校验成功后进行副WiFi网络操作。

 if (!isSlaveWifiConnected(getApplicationContext())){
            //TODO Provide Dual Wifi Ui guidance and reCheck
            Intent intent = new Intent("android.settings.DUAL_WIFI.WIFI_SETTINGS");
            startActivity(intent);
            return;
        }

2.5接入双WiFi网络part 1/4

反射获取副WiFi的transport

private int getSlaveTransportType() {
        int slaveTransportType = -1;
        try {
            Class networkCapabilitiesClass = Class.forName("android.net.NetworkCapabilities");
            Field field = networkCapabilitiesClass.getField("TRANSPORT_SLAVE_WIFI");
            slaveTransportType = (int) field.get(null);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return slaveTransportType;
    }

2.6接入双WiFi网络part 2/4

初始化副WiFi对应的NetworkRequest

 int slaveTransportType = getSlaveTransportType();
        if (slaveTransportType < 0) {
            return;
        }
        NetworkRequest nr = getNetworkRequestForType(slaveTransportType);


    private NetworkRequest getNetworkRequestForType(int type) {
        return new NetworkRequest.Builder()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .addTransportType(type).build();
    }

2.7接入双WiFi网络part 3/4

初始化副WiFi所需的NetworkCallback

SlaveCallback slaveNetworkCallback = new SlaveCallback();
    private class SlaveCallback extends ConnectivityManager.NetworkCallback {
        @Override
        public void onAvailable(Network network) {
            Log.d(TAG, "Network available: " + network.toString());
            Socket skS;
            try {
                InetAddress[] inetAddress = network.getAllByName("www.mi.com");
                skS = network.getSocketFactory().createSocket("www.mi.com", 80);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
            Log.d(TAG, "skS = " + skS.toString());
        }
    }

2.8接入双WiFi网络part 4/4

请求副WiFi网络

ConnectivityManager cm = (ConnectivityManager) getApplicationContext().
        getSystemService(Context.CONNECTIVITY_SERVICE);
cm.requestNetwork(nr, slaveNetworkCallback);

PS: manifest 添加 permission
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>

2.9 api补充

副WiFi提供的api汇总,详见“小米双wifi接口列表v1.0”

3.补充说明

3.1系统支持

当打开“双WLAN加速”并且辅助WLAN连接时,系统可以自动分配连接到不同的WiFi链路。 应用开发者可以跟小米确认开发的APP是否已经被支持。 当前已支持 :

  • 直播类:斗鱼,虎牙、企鹅电竞、抖音、快手、微视;           
  • 购物类:淘宝、天猫、京东、毒、唯品会、苏宁易购、阿里巴巴、有品、小米商城、拼多多、河马生鲜、 PP视频、咸鱼 ;        
  • 视频类:优酷、腾讯视频、哔哩哔哩、爱奇艺、影视大全、西瓜视频、芒果Tv,人人视频、搜狐视频、 电视家;       
  • 浏览器:UC浏览器、QQ浏览器、百度浏览器、搜狗、猎豹,搜狗搜索;        
  • 音乐类:网易云音乐、酷我、央视影音、咪咕音乐、酷狗、qq音乐 ;      
  • 休闲app、大准点评、知乎、最右、虎扑;      
  • 新闻类:今日头条、腾讯新闻、一点资讯、想看、新浪新闻;  
  • 其他:迅雷、58、汽车之家、懂车帝、乐视视频、链家、瓜子二手车、安居客。 

3.2应用适配 

推荐开发者参照本接入指南中的“双WiFi接入”章节和附带的demo对双WiFi进行应用的适配。双WiFi连接的情况下,系统可以在两个网卡之间对socket进行分配,对于多socket的情况,是有加速效果的。   但是这种有一个弊端,系统不知道APP的socket想被放到哪个网卡,是随机控制的,所以就可能出现跨运营商访问的情况。而应用自己做适配,应用可以自己控制数据传输到自己想传输的网卡上,这样就可以避免跨运营商访问的情况,更好地达到提升网络稳定性和数据加载速度的目的。

4.附件

小米手机设备全局拖拽功能技术适配说明

小米手机设备全局拖拽功能技术适配说明

1.简介

安卓拖拽分享功能提供了一种跨窗口传递数据的功能,文本、图像或任何可以用uri表示的数据都可以通过拖拽从一个窗口传递到另一个窗口。

可参考谷歌官方文档:Drag and drop | Android Developers

app适配拖拽功能主要分为拖出适配拖入适配,本文将分别简介其适配方法。

2.拖出适配

app对任意view调用startDragAndDrop方法即可实现拖出。本章分别对拖出文字、拖出图片、拖出任意文件进行演示。

2.1.拖出文字

使用一个TextView来拖出文字:

// 拖出文字示例
findViewById<TextView>(R.id.drag_text_view).setOnLongClickListener { view -> // 设置长按回调
    val textView = view as TextView
    val clipData = ClipData.newPlainText("label", textView.text) // 构建存放文本的ClipData
    // 调用view.startDragAndDrop方法开始拖拽
    textView.startDragAndDrop(clipData, // 传入clipData
        View.DragShadowBuilder(textView), // 使用textView的draw方法绘制拖拽的图像
        null, // 传入一个本地数据对象
        View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ) // 加入这些flag允许跨窗口拖拽
    true
}

我们只需要使用ClipData.newPlainText构建一个保存文本的clipData,再调用view.startDragAndDrop方法,将这个clipData作为参数传入,即可实现文字的拖出。

安卓的EditText本身就实现了文字的拖出和拖入,不需要额外适配。

2.2.拖出图片

使用一个ImageView来拖出图片:

// 拖出图片示例
findViewById<ImageView>(R.id.drag_image_view).setOnLongClickListener { imageView -> // 设置长按回调
    val imageUri = getFileUri(R.mipmap.drag_image, "drag_image.png") // 通过fileProvider生成图像文件uri
    val clipData = ClipData("label", arrayOf("image/png"), ClipData.Item(imageUri)) // 使用imageUri构建ClipData
    // 调用view.startDragAndDrop方法开始拖拽
    imageView.startDragAndDrop(clipData, // 传入clipData
        View.DragShadowBuilder(imageView), // 使用imageView的draw方法绘制拖拽的图像
        null, // 传入一个本地数据对象
        View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ) // 加入这些flag允许跨窗口拖拽
    true // 返回true表示长按事件被处理了
}

其中getFileUri方法将安卓资源中的一张图片作为文件保存到本地,再使用FileProvider得到图片文件的uri,如果对具体实现有兴趣可以阅读源码。最终我们是将uri放入ClipData中,再调用view.startDragAndDrop方法,将这个clipData作为参数传入,即可实现图片的拖出。

2.3.拖出任意文件

任意文件和图片一样,使用一个uri来表示,因此拖出任意文件可以使用和拖出图片类似的方法实现。

使用一个Button来选择任意文件,然后使用一个TextView来显示文件uri并实现文件的拖出:

// 拖出任意文件示例
findViewById<Button>(R.id.choose_file_button).setOnClickListener { chooseFile() } // 选择文件按钮
mDragFileView = findViewById(R.id.drag_file_view)
mDragFileView.setOnLongClickListener { // 设置长按回调
    if (mFileUri != null) { // 选择的文件保存在mFileUri,如果其不为null表示已经选择了一个文件
        val clipData = ClipData.newRawUri("label", mFileUri) // 使用mFileUri构建ClipData
        // 调用view.startDragAndDrop方法开始拖拽
        mDragFileView.startDragAndDrop(
            clipData, // 传入clipData
            View.DragShadowBuilder(mDragFileView), // 使用imageView的draw方法绘制拖拽的图像
            null, // 传入一个本地数据对象
            View.DRAG_FLAG_GLOBAL or View.DRAG_FLAG_GLOBAL_URI_READ) // 加入这些flag允许跨窗口拖拽
    }
    true // 返回true表示长按事件被处理了
}

3.拖入适配

app对任意view注册OnDragListener监听器即可实现拖入处理。本章分别对拖入文字、拖入图片进行演示。

3.1.拖入文字

使用一个TextView来拖入文字:

// 拖入文字示例
findViewById<TextView>(R.id.drop_text_view).setOnDragListener { view, event -> // 设置拖拽监听器
    val textView = view as TextView
    when (event.action) { // 对拖拽不同的事件进行处理
        DragEvent.ACTION_DRAG_STARTED -> {
            val hasText = event.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) // 查找是否有文字类型的数据
            if (!hasText) { // 没有文字类型的数据
                return@setOnDragListener false // 返回false代表这次拖拽不再继续接收拖拽事件
            }
        }
        DragEvent.ACTION_DROP -> { // ACTION_DROP事件表示拖拽抬手结束的时候
            textView.text = event.clipData.getItemAt(0).text // 将文字设到textView上显示
        }
    }
    true // 返回true代表拖拽事件被处理了
}

我们只需要通过view.setOnDragListener方法注册一个监听器,并在监听器里面处理拖拽事件即可实现拖入。其中在ACTION_DRAG_STARTED事件中对数据类型进行判断,如果不是我们想要的数据类型就返回false即可不再接收本次拖拽事件;最后在ACTION_DROP事件中获取ClipData数据并进行相应的处理。

3.2.拖入图片

使用一个ImageView来拖入文字:

// 拖入图片示例
findViewById<ImageView>(R.id.drop_image_view).setOnDragListener { view, event -> // 设置拖拽监听器
    val imageView = view as ImageView
    when (event.action) { // 对拖拽不同的事件进行处理
        DragEvent.ACTION_DRAG_STARTED -> {
            val mimeTypes = event.clipDescription.filterMimeTypes("image/*") // 查找是否有图像类型的数据
            if (mimeTypes == null) { // 没有图像类型的数据
                return@setOnDragListener false // 返回false代表这次拖拽不再继续接收拖拽事件
            }
        }
        DragEvent.ACTION_DROP -> { // ACTION_DROP事件表示拖拽抬手结束的时候
            requestDragAndDropPermissions(event) // 申请读取uri的权限
            imageView.setImageURI(event.clipData.getItemAt(0).uri) // 将图像uri设到imageView上显示
        }
    }
    true // 返回true代表拖拽事件被处理了
}

与拖入文字不同的地方在于,图片是一个用uri表示的文件,要访问这个uri之前必须调用Activity.requestDragAndDropPermissions方法申请权限。

3.3.拖入任意文件

类似于拖入图片,想要拖入任意文件只需要对拖过来的任意uri进行处理即可,示例代码如下:

// 拖入任意文件示例
view.setOnDragListener { view, event -> // 设置拖拽监听器
    when (event.action) { // 对拖拽不同的事件进行处理
        DragEvent.ACTION_DROP -> { // ACTION_DROP事件表示拖拽抬手结束的时候
            requestDragAndDropPermissions(event) // 申请读取uri的权限
            // 处理uri
        }
    }
    true // 返回true代表拖拽事件被处理了
}

3.4.判断拖入的数据类型

拖入方通常有2种方式判断拖入数据类型。

方法1:可以根据event.clipDescription中的MIMETYPE来判断数据类型

MIMETYPE本身是一个字符串,谷歌对其在ClipDescription.java中有一些预定义:

public class ClipDescription implements Parcelable {
    /**
     * The MIME type for a clip holding plain text.
     */
    public static final String MIMETYPE_TEXT_PLAIN = "text/plain";

    /**
     * The MIME type for a clip holding HTML text.
     */
    public static final String MIMETYPE_TEXT_HTML = "text/html";

    /**
     * The MIME type for a clip holding one or more URIs.  This should be
     * used for URIs that are meaningful to a user (such as an http: URI).
     * It should <em>not</em> be used for a content: URI that references some
     * other piece of data; in that case the MIME type should be the type
     * of the referenced data.
     */
    public static final String MIMETYPE_TEXT_URILIST = "text/uri-list";

    /**
     * The MIME type for a clip holding an Intent.
     */
    public static final String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent";

    /**
     * The MIME type for an activity. The ClipData must include intents with required extras
     * {@link #EXTRA_PENDING_INTENT} and {@link Intent#EXTRA_USER}, and an optional
     * {@link #EXTRA_ACTIVITY_OPTIONS}.
     * @hide
     */
    public static final String MIMETYPE_APPLICATION_ACTIVITY = "application/vnd.android.activity";

    /**
     * The MIME type for a shortcut. The ClipData must include intents with required extras
     * {@link Intent#EXTRA_SHORTCUT_ID}, {@link Intent#EXTRA_PACKAGE_NAME} and
     * {@link Intent#EXTRA_USER}, and an optional {@link #EXTRA_ACTIVITY_OPTIONS}.
     * @hide
     */
    public static final String MIMETYPE_APPLICATION_SHORTCUT = "application/vnd.android.shortcut";

    /**
     * The MIME type for a task. The ClipData must include an intent with a required extra
     * {@link Intent#EXTRA_TASK_ID} of the task to launch.
     * @hide
     */
    public static final String MIMETYPE_APPLICATION_TASK = "application/vnd.android.task";

    /**
     * The MIME type for data whose type is otherwise unknown.
     * <p>
     * Per RFC 2046, the "application" media type is to be used for discrete
     * data which do not fit in any of the other categories, and the
     * "octet-stream" subtype is used to indicate that a body contains arbitrary
     * binary data.
     */
    public static final String MIMETYPE_UNKNOWN = "application/octet-stream";
    ......
}

这里面只定义了部分数据,还有其它数据需要app自己定义,目前没有一个准确的规范,通常来说如果是格式为jpg的图像数据则MIMETYPE为”image/jpg”。所以MIMETYPE可以用来初步判定数据类型,但是不完全准确,毕竟MIMETYPE是由拖出方设定的值。

方法2:可以对传入的uri来判断数据类型

我们可以使用ContentResolver的getType方法获取uri的MIMETYPE,然后可以通过MimeTypeMap的getExtensionFromMimeType方法获取文件后缀名。

val mimeType = contentResolver.getType(uri) // 获取MIMETYPE
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) // 获取文件后缀名

通过文件后缀名我们就可以准确判断数据类型。

小米手机设备全局自由窗口适配说明

小米手机设备全局自由窗口适配说明

1.自由窗口简介

  • 小窗
  • 迷你小窗

2.为什么要做“自由窗口”

  • 在多任务处理场景下,小窗意在解决临时使用某个应用的场景,例如:在使用游戏应用时,不便离开,但想发消息给朋友,此时就可以借助小窗打开第二应用;
  • 迷你小窗意在解决应用临时挂机的场景,例如:等待网约车、等待游戏更新、观看直播等。

3.如何使用“自由窗口”

4.自由窗口技术适配指南

  • MIUI的小窗是基于Android的多窗口Freeform方案实现的。
  • 小窗目前主要问题是应用兼容性导致的一系列问题,内容显示不全、Touch事件不响应等等,其实根本原因是应用没有很好的支持、适配多窗口、多分辨率,如下是小窗适配的一些参考性适配指南。

4.1 google多窗口开发适配指南

多窗口适配支持文档:

https://developer.android.com/guide/topics/ui/multi-window#dnd

支持不同屏幕分辨率的开发技巧文档:

https://developer.android.google.cn/training/multiscreen/screensizes

4.2 应用声明是否支持自由窗口

如果您的应用以 API 级别 24 或更高级别为目标平台,那么您可以配置该应用的 activity 是否支持以及如何支持多窗口显示,自由窗口也是google多窗口的一种,所以goolge多窗口适配方案同样适用,可以在清单的<activity><application>元素中设置此属性,以启用或停用多窗口显示:

android:resizeableActivity=["true" | "false"]

如果将此属性设置为 true,则activity 能以分屏和自由窗口模式启动。如果将此属性设置为 false,则 activity 不支持多窗口模式。如果此值为 false,并且用户尝试在多窗口模式下启动 activity,则 activity 会全屏显示。

如果您的应用以 API 级别 24 为目标平台,但您未指定此属性的值,则其值默认设为 true。

4.3 判断应用进入退出自由窗口模式

public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
}

每当 Activity 进入或退出多窗口模式时,系统都会调用此方法。当 Activity 进入多窗口模式时,系统会向该方法isInMultiWindowMode传递 true 值,退出多窗口模式时则传递 false 值。

可以通过newConfig中的mWindowingMode窗口模式来判断是否进入退出自由窗口,其中自由窗口的mWindowingMode为freeform,全屏的mWindowingMode为fullscreen,其他窗口模式可打印newConfig来查看,其中mWindowingMode是非public的,可以通过newConfig.toString().contains(“mWindowingMode=freeform”)间接获取。

4.4 获取自由窗口的Task大小

public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) {
}
public void onConfigurationChanged(Configuration newConfig) {
}

可以通过newConfig 对象中的mBounds以及sw获取自由窗口的窗口大小,应用可以通过窗口的大小进行UI布局适配。

4.5 小窗上操作杆区域遮挡应用内容适配方案

  • 应用全屏沉浸模式

app的根布局加上 android:fitsSystemWindows=”true”

应用全屏非沉浸模式

不需要处理

4.6 焦点窗口适配

迷你小窗和小窗都有可能成为不是焦点窗口(Focus Window),小窗、迷你窗和底部的全屏应用是可以随意进行焦点切换的,操作哪个窗口哪个窗口就是焦点窗口,所以就要保证应用在没有焦点的前提下也是可以正常运行的,特别是视频,游戏类app,比如说某款游戏在失去焦点时会停止绘制,造成游戏界面黑屏等问题。焦点窗口进行切换时系统会回调Activity.onWindowFocusChanged(boolean hasFocus)方法。

4.7 Stop Playback

问题描述:

在切换到Multi Window状态下焦点发生变化/Switch/Resize时视频停止播放。

修改建议:

播放视频的Activity不要在其onPause()中暂停视频,建议在onStop()中处理暂停,在onStart()中播放。

从Android P开始,应用可以增加支持Multi-resume属性,支持以后,在多窗口中的应用都会处于Resume状态,而非pause状态。

4.8 小窗上下杆子的背景色

应用可以通过设置状态栏,导航航栏的颜色调整上下杆子的背景色,但是不是绝对的,

横屏游戏等特殊场景上下杆子的背景色系统侧做了处理,是固定不变的。

4.9 适配Activity大小切换时不重启, 正确处理Configuration变化

强烈建议应用适配Activity窗口大小切换时不重启,遵循google规范,在android:configChanges 属性增加 screenSize|screenLayout|orientation|smallestScreenSize,并在Activity的onConfigurationChanged回调中更新宽高刷新各个子布局。

4.10 Display APIs

  • 获取屏幕大小

R版本之前:Display.getRealSize()、Display.getRealMetrics()。

R版本之后:WindowManager.getMaximumWindowMetrics() 。

  • 获取当前窗口大小

R版本之前: Display.getSize() 。

R版本之后:WindowManager.getCurrentWindowMetrics() 。

4.11 注意事项

所有的Configuration对象要用接口入参newConfig对象做相关的处理逻辑,不要通过context.getResources().getConfiguration()等方式去获取,可能存在Configuration内容还没有被系统更新的情况。

小米分屏功能适配说明

小米分屏功能适配说明

1. 前言

从 7.0 开始,Android 加入了新的特性 Multi-Window,以支持同时显示多个应用,根据不同场景及设备,细分为以下三种:

  • Split-Screen Mode,即分屏,用于手机和平板
  • Picture-in-Picture Mode,即画中画,用于电视端(Android N)及移动端(Android O)
  • Freeform Mode,自由模式,用于在更大尺寸的设备上自由缩放应用窗口

可以看到,分屏是 Multi-Window Mode 的其中一种,用于手机等移动设备。MIUI 从 MIUI 9 开始支持分屏,基于 Android 7.0 优化,因此开发者不用针对 MIUI 做重复的适配工作。

2. MIUI 分屏的表现

  • 默认将窗口一分二,支持调整窗口大小。
  • 支持横屏
  • 支持主应用始终在前台
  • 对于折叠屏设备,在屏幕展开状态下,分屏模式为左右分屏,而且宽度支持多档位调节

3. 分屏对开发者的好处是

如前所述,Android 7.0 的分屏允许某个应用始终可见,MIUI 会在这个基础上做更多的交互优化,以符合用户的预期。在这个背景下,分屏对于开发者来说有以下好处:

  • 可能提升使用时长:由于应用始终可见,主流程不会被打断,有助于提升使用时长。
  • 可能提升日均使用次数:使用分屏后,消费长内容(如长视频)的成本变低了,用户不用想着留一整段时间,也有助于提升使用次数。

随着 Android 7.0 的设备越来越多,支持分屏带来的好处将会被不断放大。加之 Android 有标准的分屏接口,大大减少了开发者的适配工作量。

4. 如何支持分屏

支持分屏的方式非常简单,只需要声明一个属性。从 7.0 开始,Android 新增了一个 Activity 属性: resizeableActivity ,以声明该 Activity 是否支持多窗口显示。

android:resizeableActivity=["true" | "false"]

如果这个属性设为 true ,Activity 就可以在分屏模式下显示;设为 false ,Activity 则不会在分屏模式下显示,而是会占满整个屏幕。因此开发者可以根据具体场景,仅让部分 Activity 支持分屏。

若开发者没有为 Activity 声明该属性,Android 会根据应用的 targetSDKVersion 及 Activity 的 screenOrientation 属性来综合判断是否可以在分屏显示。关于判断的详细逻辑,可以参考官方文档 Configuring Your App for Multi-Window Mode 或者这篇更详细的博客 Android N7.0多窗口适配开发指导 。

5. 如何进一步优化分屏模式

配置 resizeableActivity 的属性,是适配分屏的最简单方式,但如果想要提供更好的使用体验,需要开发者做一定优化工作。下面是我们了解到的一些案例(测试机型:Nexus 6 Andriod 7.1),开发者可以根据自己的业务需求,做不同程度的优化。

5.1. 减少不可滑动的页面/控件

在分屏过程中,屏幕高度只有原来的一半,如果有太多的控件不响应滑动事件,那么用户将无法上下滚动应用页面,甚至无法进行下一步操作。这类页面,最常见于Splash screen、登录注册页、音乐播放页、大图区域、弹窗等。

由于用户可以自由调整分屏的窗口比例,因此开发者只要减少了不可滑动的控件,分屏的可用性就会大大提高,是性价比非常高的优化方案。

5.2. 尽可能使用相对位置,以兼容多种窗口尺寸

分屏时,屏幕的高度和宽度会发生变化,因此在书写控件布局时,尽量使用相对位置,以避免窗口大小改变时,控件无法显示或显示不全。这也是一种性价比很高的优化方案,可以保证用户在分屏时能正常使用应用。

5.3. 注意多窗口下 Activity 的生命周期

视频、直播等类别的应用需要特别关注这一点。Android 7.0 在分屏时会同时运行两个应用,其中用户最后操作的那个应用会处于 Resumed 状态,另一个则会处于 Paused 状态。

这会带来一些问题,以视频应用为例,如果开发者在 onPaused 中处理视频的 「暂停/播放」,那在分屏时,就会因为用户操作另一个应用,导致视频停止播放。因此我们建议开发者在 onStart/onStop 中处理视频的「暂停/播放」,或者特殊处理分屏时的 Paused 状态。详见官方文档 Multi-Window Lifecycle

5.4. 处理 Configuration Changes

由于分屏过程中,允许用户调整窗口的大小,这就会导致 Configuration 的改变。Android 的默认处理方式是 relaunch 整个 Activity,从而出现页面闪一下的问题。如果想避免闪一下的问题,建议开发者自己处理 Configuration 的变化。

5.5. 给内容更多空间

分屏后,屏幕空间变得非常小了,为了给内容让出更多空间,应尽量减少常驻控件。一种解决办法是在浏览内容时,隐藏底部tab等常驻控件,用户回滚时再出现,以展示更多的内容。

5.6. 为分屏定制新的布局(动态布局)

动态布局指根据当前的窗口大小,重新调整页面的布局。这是一项锦上添花的优化项,开发者可以酌情考虑是否添加此优化。

6.FAQ

6.1. MIUI 分屏支持哪些设备

搭载 Android 7.0 或以上的 MIUI 手机及平板设备均支持分屏。MIUI 也将尝试将分屏移植至 Android 6.0。因此将有数千万的新老设备支持分屏。

6.2. 如何调试

MIUI 的分屏方案完全兼容 Android 7.0,因此可以在任意运行 Android 7.x 的设备上调试,无需为 MIUI 作特别处理。同时,以上提到的案例均能在任意 Android 7.x 设备复现。调试过程中,建议开启以下设置项:「开发者选项 > 强制将活动设为可调整大小」,然后重启手机,之后系统就会强制应用进入分屏模式,以方便开发者观察应用在分屏时的表现。

6.3. 分屏是个伪需求吗,用户为什么需要分屏

分屏不仅不是伪需求,而且会是一个大众需求。我们认为分屏最主流的使用场景是:一边看视频,一边做其它事情。用手机看视频,已经成为用户的主流场景,各大视频应用的日活人数、使用次数、使用时长都可以佐证这个观点。但使用手机看视频有很多痛点,比如会被 IM 消息打断、切换至后台视频会暂停等。这些痛点都可以通过分屏较好地解决,这也是分屏能成为大众需求的潜力。

但不可否认的是,分屏也会带来很多可用性问题,尽管系统已经做了很多优化工作,仍然需要各大开发者做进一步的优化,提高分屏模式的可用性。因此非常希望各位开发者能够支持该功能,为广大用户提供更好的使用体验。再次感谢所有开发者的支持与付出!

6.4. 参考资料