小米开发平台后台弹出页面权限管理说明
1. 介绍
安卓系统中,由于三方应用可以随意从后台弹出页面,严重影响用户体验,该权限可以控制应用是否可以在后台启动页面。
2. 原则
该权限默认为拒绝的,既为应用默认不允许在后台弹出页面,针对特殊应用会提供白名单,例如音乐(歌词显示)、运动、VOIP(来电)等;
白名单应用一旦出现推广等恶意行为,将永久取消白名单。

小米开发平台后台弹出页面权限管理说明
安卓系统中,由于三方应用可以随意从后台弹出页面,严重影响用户体验,该权限可以控制应用是否可以在后台启动页面。
该权限默认为拒绝的,既为应用默认不允许在后台弹出页面,针对特殊应用会提供白名单,例如音乐(歌词显示)、运动、VOIP(来电)等;
白名单应用一旦出现推广等恶意行为,将永久取消白名单。
小米开发平台自启动权限管理说明
自启动是指手机开机后,应用会未经用户允许自动启用。
在用户手机使用过程中,当自启动应用过多时,会造成手机耗电加快,内存占用过高等情况,造成用户使用体验下降。
为防止应用自启动数量过多,占用用户手机内存,造成用户手机耗电加快等现象发生,手机将默认应用不可自启动,且应用自启动功能需告知用户。如有需要,用户可以自行设置应用允许应用自启动。
小米手机锁屏显示权限管理说明
安卓系统中,由于三方应用在锁屏上推广告,严重影响用户体验,该权限可以控制应用是否可以在锁屏上显示界面。
该权限默认为拒绝的,既为应用默认不允许在锁屏上显示界面,针对特殊应用会提供白名单,音乐(歌词显示)、运动、VOIP(来电), 白名单应用一旦出现推广等恶意行为,将永久取消白名单。
小米手机无障碍权限管理说明
无障碍服务(Accessibility Service)又称为辅助功能服务,出发点是让应用通过该接口给残障人士提供便捷的服务。但近年来,辅助功能服务愈发有被滥用的趋势,部分应用利用这些服务和API创建各种“自动化机制”强化功能。
2017年11月,谷歌邮件已通知所有应用开发者:除非开发者能明确澄清App通过使用辅助功能服务是用来帮助残障人士更好的使用安卓设备和应用。否则将拒绝其使用该服务接口的请求,并将其应用程序从Play商店中移除。
小米手机设备管理器权限管理说明
设备管理器权限是Android提供给(企业)设备管理类应用的设备保护功能,对手机设备进行管理和操作的接口权限。
权限接口涉及对用户数据、密码的操作,安全性风险极高。
部分应用滥用权限声明来进行防卸载保护,应用内并无相关功能,当用户开启后就无法卸载。
小米手机默认桌面应用管理说明
安卓系统中,由于第三方桌面类的应用不稳定,会带来手机系统卡顿、手机功耗大、偷偷下载应用、恶意扣费等问题,给用户带来了很大的使用困扰。
桌面作为系统常用、基础并承担着入口安全责任的重要应用,为保证系统的稳定、完整、一致性,小米方强制禁止三方应用设为默认桌面,只允许使用系统桌面。
小米关于调整“获取应用列表”权限&新增“调节媒体音量”权限的适配说明
“获取应用列表”权限是在Android原生 QUERY_ALL_PACKAGES 权限基础上额外的权限管理。
影响 PackageManager#getInstalledPackages和PackageManager#getInstalledApplications 两个接口返回值。
应用没适配的情况下,默认添加权限状是“仅在使用中允许”(应用不声明也会添加),即应用在前台时可以正常获取。也可以按照下面方法适配动态申请。
清单文件声明
<uses-permission android:name="com.android.permission.GET_INSTALLED_APPS"/>
判断MIUI 是否支持动态申请权限
try {
PermissionInfo permissionInfo = getApplicationContext().getPackageManager().getPermissionInfo("com.android.permission.GET_INSTALLED_APPS", 0);
if (permissionInfo != null && permissionInfo.packageName.equals("com.lbe.security.miui")) {//MIUI 系统支持动态申请该权限
if (ContextCompat.checkSelfPermission(getApplicationContext(), "com.android.permission.GET_INSTALLED_APPS") != PackageManager.PERMISSION_GRANTED) {
//没有权限,需要申请
ActivityCompat.requestPermissions(MainActivity.this, new String[]{"com.android.permission.GET_INSTALLED_APPS"}, 999);
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
提示:如果系统侧不支持此权限,应用仍然会保持默认(仅在使用中允许)。
判断权限授予结果(与普通运行时权限没有区别)
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// do something
}
如果不需要系统添加获取应用列表的权限,可以通过下面代码实现(同时,清单文件中不能申请更改权限)
<meta-data
android:name="do_not_need_get_installed_apps"
android:value="true"/>
注意:如果声明此meta-data,等于“获取应用列表”权限关闭!
MIUI 13
为减少部分应用擅自修改媒体音量,而不主动告知用户或获得用户许可的行为,同时尽可能避免对用户带来相应的困扰。现在单独设立“调节媒体音量”的权限,并将默认状态设置为“仅在使用中允许”。同时,“应用行为记录”功能中将会对应用修改媒体音量的行为进行记录,用户可主动调整相关的权限。您的应用如有调节媒体音量的需求,可能受到影响,建议您自行调整相关的产品策略。
MIUI 11及以下版本将不受该调整的影响。
小米开发平台隐身模式三方应用适配文档
说明:若不涉及到使用录音、相机、麦克风权限,则可忽略此项适配
功能效果:
若三方应用在隐私模式开启状态下,因无法使用定位、相机、麦克风而无任何提示,且用户忽略了系统弹出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”
若想体验该功能,机型和版本要求如下:
机型:小米10、小米9、红米Redmi K40游戏增强版、红米Redmi K30至尊纪念版、红米Redmi 9、MIX FOLD
MIUI版本:21.6.5之后的开发版rom
小米开放平台MIUI进程管理适配说明
应用进程的存活与否常常受到三方开发者的关注。与原生系统不同,MIUI在Android系统的基础之上,开发了一套进程管理模块,便于系统管理运行中的进程。
此文档将会提供进程相关信息,方便开发者适配MIUI进程管理机制、初步自查应用被杀原因、更精确的向我们的三方团队同事反馈应用异常被杀问题
MIUI的进程管理功能大致分为两类:用户主动触发、用户被动触发
用户主动触发的功能包含:
| 名称 | 触发入口 | Reason |
| 一键清理 | 最近任务/悬浮球 | OneKeyClean |
| 强力清理 | 负一屏 | ForceClean |
| 垃圾清理 | 安全中心 | GarbageClean |
| 锁屏清理 | 安全中心 | LockScreenClean |
| 游戏清理 | 安全中心 | GameClean |
| 优化清理 | 安全中心 | OptimizationClean |
| 上滑清理 | 最近任务 | SwipeUpClean |
如果发现应用因为以上原因被杀死,那么意味着是用户在触发入口主动杀死这些应用
用户被动触发的功能包含:
| 名称 | 被动触发场景 | Reason |
| Power异常查杀 | 应用过度耗电 | AutoPowerKill |
| Thermal异常查杀 | 应用使手机发热 | AutoThermalKill |
如果发现应用因为以上原因被杀死,那么意味着应用出现异常,会影响到系统正常运作,系统将应用清理掉了
可以在shell中输入命令:
adb logcat -b events | grep am_kill
查看被杀应用的日志,例如:
1494 2963 I am_kill : [0,5253,com.eg.android.AlipayGphone,500,LockScreenClean]
最后一列信息即为被杀的Reason,和上面的表格进行一下对比,即可定位被杀的原因
当出现应用频繁被被动原因杀时的情况时,开发者首先应该自己检查下自身应用有没有过度耗电、发热的行为
如果确认自身应用质量没有问题,可以打个bugreport并联系我们的三方团队的同事进行反馈,我们内部的对应开发会进行深度分析
开发者可以在用户使用应用时,引导用户在安全中心中打开自启动开关
小米手机目前高通平台机型上提供K歌低延时耳返功能 ,用户可以在小米手机上体验震撼的K歌效果,诚邀应用开发者适配,感谢支持!
支持耳返功能的设备
| itgsa接口 | 小米12S, 小米12S Pro,小米12S Ultra和其他出厂系统为Android 13版本的高通平台机型 |
| 小米接口 | 其他高通平台机型 |
需要应用权限配置:
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.RECORD_AUDIO
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系统
小米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均衡器音效。 |
开启小米相册的安全分享功能后,从相册分享图片会自动抹除位置、手机型号、拍摄参数等信息。
安全分享开关: 相册->右上角菜单->设置->安全分享
小米相册中打开安全分享中任意一个开关(默认抹除照片位置、默认抹除照片拍摄信息)后,从相册分享时会将抹除相关信息的照片保存到相册的私有目录,然后通过FileProvider的方式生成content://Uri分享给应用
开启安全分享后分享的照片路径:
storage/emulated/0/Android/data/com.miui.gallery/cache/SecurityShare/XXX.jpg
未开启安全分享功能分享的照片路径:
storage/emulated/0/DCIM/Screenshots/XXX.jpg
由于从Android11开始,应用的私有目录不能被外部访问,即使获取了“所有文件管理”权限也不行。具体参见https://developer.android.google.cn/about/versions/11/privacy/storage。因此如果在用户打开安全分享的情况下,应用接收到相册传递给应用的URL,获取到绝对路径,再直接通过访问绝对路径的方式来读取照片,会发生读取异常。
例如以下错误
BitmapFactory: Unable to decode stream: java.io.FileNotFoundException: /storage/emulated/0/Android/data/com.miui.gallery/cache/SecurityShare/1655174005823.jpg: open failed: ENOENT (No such file or directory)
针对开启安全分享的情况下,分享照片到应用,建议应用使用fileprovider(https://developer.android.com/reference/androidx/core/content/FileProvider)的方式访问照片,参考代码如下:
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
if (Intent.ACTION_SEND.equals(action) && type != null) {
if (type.startsWith("image/")) {
Uri imageUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
InputStream is = null;
try {
is = getContentResolver().openInputStream(imageUri);
Bitmap bmp = BitmapFactory.decodeStream(is);
imageView.setImageBitmap(bmp);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
当手机放置在跑步机上时,收集手机中传感器的数据,判断是否在跑步机上运动,若运动一步则跑步机传感器上报一次数据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);
}
}
在代码中创建一个包名为android.os,类名为ServiceManager的类。
代码示例:
package android.os;
public class ServiceManager {
private ServiceManager(){}
public static IBinder getService(String name){
return null;
}
}
目前跑步机计步传感器支持小米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();
小米开发平台小米计步器接口适配说明
收集手机中传感器的数据,通过机器学习算法判断步数。
使用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);
接口格式:
Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
| 参数 | 必选 | 类型 | 范围 | 说明 |
| uri | true | android.net.Uri | Uri.parse(“content://” + “com.miui.providers.steps” + “/item”); | 取固定值 |
| projection | false | String[] | 可选列: “_id”, “_begin_time”, “_end_time”, “_mode”, “_steps” | 获取指定列, 若为null则获取所有列 |
| selection | false | String | 从可选列中选取自定义条件 | 获取满足条件的记录行, 若为null则获取所有行 |
| selectionArgs | false | String[] | 一般为可选列的值 | Selection中带?的格式化参数 |
| sortOrder | false | String | 从可选列中选取列升序(asc)或降序(desc), 可多个并列 | 返回结果的记录行排序方式, 若为null则按id大小排序 |
记录行的数据结构如下
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; // 总步数
}
<uses-permission android:name=”miui.permission.READ_STEPS” />(不声明权限无法读取计步数据)。
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
};
public class Step {
private int id;
private long mBeginTime;
private long mEndTime;
private int mMode;
private int mSteps;
}
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;
}
小米开发平台小米妙播适配说明
Android 框架定义了两个类(媒体会话和媒体控制器),它们为构建媒体播放器应用提供了一个完善的结构。
媒体会话和媒体控制器通过以下方式相互通信:使用与标准播放器操作(播放、暂停、停止等)相对应的预定义回调,以及用于定义应用独有的特殊行为的可扩展自定义调用。
更多信息详见:https://developer.android.com/guide/topics/media-apps/media-apps-overview
小米妙播支持基于Media Session的音频播控、基于Wi-Fi的跨设备音乐接力。
用户可以在小米手机和平板的系统控制中心、通知栏/锁屏媒体中心使用该功能。
控制中心内的小米妙播播控,基于安卓原生Media Session能力实现,音频应用适配Media Session即可,众多知名三方应用均已适配。
小米妙播作为控制中心常驻的播控、互联入口,用户可以在手机全局操作音频,是Android厂商中全面媲美苹果 AirPlay2 的系统级功能。
各音频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
适配要点:
// 构建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是一套振动波形的生成框架,可以通过参数或者HE格式为你的线性马达机器定制出丰富的振动效果。
MiHaptic支持接入不同的触感波形生成算法以驱动手机上的线性马达,目前支持小米自研算法和RichTap。
QQ音乐节奏实验室4D振感,开启音乐随振,让听歌更有节奏感。
和平精英高品质振动使用MiHaptic方案对不同枪械、载具、脚步声、玻璃破碎等场景提供拟真的触感反馈。
提供组合PrimitiveEffect的方式。
提供HE文件振动方式。
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,后面的计算方式同理。
{
"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扫一扫 -> 画面一直无法聚焦
对焦失败的原因:第一次AFtrigger被reconfigure冲掉了,导致AF模块状态异常,一直无法对焦。
扫一扫采用auto方式对焦,即先设置AFMode=auto,然后周期性不断下发aftrigger(loop focus)来主动触发每一次对焦。原理如下:
图1 auto对焦原理图
configure_stream是Cameraservice的行为。在open camera初始化阶段,Cameraservice会根据app的对分辨率、画幅、stream等需求向底层发送configure_stream的指令。通俗的讲,比如某个camera需要预览和yuv数据,那么在初始化阶段就需要将这些需求通知底层camera,Camera会根据这些需求搭建对应的硬件和软件资源,当底层准备完毕后,就可以开始接收app的request需求,并返回数据。
当Cameraservice检测到app的需求发生变化时,便会触发reconfigure,将新的需求向底层发送configure_stream的指令。例如:一开始app只需要预览,那么Cameraservice就会向底层configure预览流,底层就会搭建好预览流的输出通道,过了几帧后,app又需要yuv了,于是就会执行reconfigure,此时会将上次configure的资源全部清除,并重新建立预览+yuv的通道。
以微博扫一扫为例分析不对焦的原因,不同app行为可能有所不同。
图2 微博扫一扫对焦流程图
一般对于扫码的场景,app会请求预览流(用于显示)和yuv流(用于处理、识别二维码),但由于yuv并不需要马上使用,因此,预览和yuv的请求可能会存在时间差。
根据以上分析可知,不对焦有两个条件,一是触发了reconfigure,二是reconfigure冲掉了第一次AFtrigger。因此对于很多app,reconfigure的问题一直存在,但reconfigure的时机具有随机性,如果第二次configure发生在第一次AFtrigger之前,就不会冲掉trigger,也就不会引起问题。
通过与微博研发人员合作验证以下两种方案均可修复该问题:
建议方案2,可以根源上解决问题。如下是微博的修复方案(不同app可能情况不一样,仅供参考):在startpreview()前执行setPreviewCallback(YUV)。
说明:在开启预览时app会调用startpreview()接口,如果在此之前调用setPreview Callback(),提前设置yuv的callback,这样便可以在预览时同时请求yuv流。否则,startpreview()只会请求预览流,当执行到setPreviewCallback(YUV),会触发reconfigure请求yuv。
小米开发平台MIUI无极音量适配说明
MIUI为了提升用户的音频体验,将媒体音音量范围从[0,15]修改为[0,150]。用户在滑动音量条时,每一次轻微的滑动都能改变音量,细微的音量调节让用户能找到更加合适的音频响度。
App应内部自己定义步长step,进而算出index。
若支持无极音量step = 10,若不支持step = 1;原因是为了兼容不支持无极音量的机型,统一定义:按键15次将音量调满。
统一计算公式 step = MaxVolume /15;
MaxVolume = AudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)。
总结:简单来说,应用App如果有自己的音量调节逻辑,应该内部定义步长step。通过以上方式可兼容任意机型。
MIUI 期待您的适配,若有任何问题,请及时与我们沟通:liuxiaoyu7@xiaomi.com。
小米开放平台相机各场景下3A操作适配说明
在预览时AF可以由App控制触发对焦,也可以底层自动对焦,推荐使用底层自动对焦。
可以先检测AF有效模式,如果支持CONTINUOUS_PICTURE,设置即可,具体如下:
mCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES),CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE))。
CONTROL_AF_MODE设置为CONTROL_AF_MODE_CONTINUOUS_PICTURE。
mCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES),CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO))。
CONTROL_AF_MODE设置为CONTROL_AF_MODE_CONTINUOUS_VIDEO。
CONTROL_AF_MODE_CONTINUOUS_PICTURE和CONTROL_AF_MODE_CONTINUOUS_VIDEO的区别是前者在AF时会快速收敛,后者在AF时慢速收敛,因为录制视频时为了防止图像抖动,收敛过程需要慢一点,共同点是二者都为自动对焦。
Touch是为了在预览时对特定的区域进行3A权重提高,以便于得到预期的图像。当touch预览窗口某区域时,通用的操作流程是设置CONTROL_AF_REGIONS,CONTROL_AF_TRIGGER的流程还是CANCEL – START – IDLE。
Flash的实用场景通常在Touch,拍照和录像情况下使用,flash有两种操作模式,手动模式和自动模式,具体操作如下:
当需要打开flash时,设置CONTROL_AF_MODE为OFF/AUTO模式,同时设置FLASH_MODE为torch即可。
自动模式是flash交给底层AE算法控制,具体如下两种常用模式:
ON_AUTO_FLASH:设置AF_Mode为此模式表示flash由AE根据当前亮度进行打开,关闭。
ON_ALWAYS_FLASH:设置AF_Mode为此模式表示flash每次操作都会打开。
拍照操作流程通常为触发3A,等待3A收敛,获取到3A收敛结果,进行拍照请求。
CONTROL_AE_MODE根据是否需要闪光灯设置为ON_AUTO_FLASH,ON_ALWAYS_FLASH,ON。
CONTROL_AE_PRECAPTURE_TRIGGER设置建议 CANCEL-START-IDLE。
CONTROL_AF_MODE设置为AUTO,CONTINUOUS_VIDEO都可以。
CONTROL_AF_TRIGGER设置建议CANCEL-START-IDLE。
根据CaptureResult的metadata获取3A状态,如果3A都收敛了即可请求拍照
result.get(CaptureResult.CONTROL_AF_STATE); //AF_State
result.get(CaptureResult.CONTROL_AE_STATE); //AE_State
result.get(CaptureResult.CONTROL_AWB_STATE); //AEB_State
CONTROL_AE_MODE根据是否需要闪光灯设置为ON_AUTO_FLASH,ON_ALWAYS_FLASH,ON。
CONTROL_AF_MODE设置为AUTO,CONTINUOUS_VIDEO都可以。
最终下发拍照请求:
mCaptureSession.capture(request, mCaptureCallback, mBackgroundHandler);
小米开放平台双WiFi适配说明
双WiFi顾名思义提供了手机同时连接两个WiFi同时上网的能力,一般手机只能同时连接AP的一个WiFi频段获取 WiFi信号,而支持双WiFi技术的手机可以同时连接AP的两个频段(2.4Ghz或5GHz)获取WiFi信号(可以连接一个AP的两个频段,也可以连接两个AP的不同频段)。
小米10、小米10 Pro、Redmi K30 5G、Redmi K30 Pro。
双WiFi UI操作界面可从手机如下操作路径:“设置->WLAN->WLAN助理->双WLAN加速”进入。该界面称为副WiFi界面,副WiFi界面的打开和连接与主WiFi除了频段差异外并无区别(当主WiFi连接5Ghz时,副WiFi只能连接2.4Ghz,反之主WiFi连接2.4Ghz的时候,副WiFi只能连接5Ghz)。如下图所示:
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;
}
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");
}
判断当前副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;
}
反射获取副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;
}
初始化副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();
}
初始化副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());
}
}
请求副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"/>
副WiFi提供的api汇总,详见“小米双wifi接口列表v1.0”
当打开“双WLAN加速”并且辅助WLAN连接时,系统可以自动分配连接到不同的WiFi链路。 应用开发者可以跟小米确认开发的APP是否已经被支持。 当前已支持 :
推荐开发者参照本接入指南中的“双WiFi接入”章节和附带的demo对双WiFi进行应用的适配。双WiFi连接的情况下,系统可以在两个网卡之间对socket进行分配,对于多socket的情况,是有加速效果的。 但是这种有一个弊端,系统不知道APP的socket想被放到哪个网卡,是随机控制的,所以就可能出现跨运营商访问的情况。而应用自己做适配,应用可以自己控制数据传输到自己想传输的网卡上,这样就可以避免跨运营商访问的情况,更好地达到提升网络稳定性和数据加载速度的目的。
小米手机设备全局拖拽功能技术适配说明
安卓拖拽分享功能提供了一种跨窗口传递数据的功能,文本、图像或任何可以用uri表示的数据都可以通过拖拽从一个窗口传递到另一个窗口。
可参考谷歌官方文档:Drag and drop | Android Developers
app适配拖拽功能主要分为拖出适配和拖入适配,本文将分别简介其适配方法。
app对任意view调用startDragAndDrop方法即可实现拖出。本章分别对拖出文字、拖出图片、拖出任意文件进行演示。
使用一个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本身就实现了文字的拖出和拖入,不需要额外适配。
使用一个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作为参数传入,即可实现图片的拖出。
任意文件和图片一样,使用一个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表示长按事件被处理了
}
app对任意view注册OnDragListener监听器即可实现拖入处理。本章分别对拖入文字、拖入图片进行演示。
使用一个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数据并进行相应的处理。
使用一个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方法申请权限。
类似于拖入图片,想要拖入任意文件只需要对拖过来的任意uri进行处理即可,示例代码如下:
// 拖入任意文件示例
view.setOnDragListener { view, event -> // 设置拖拽监听器
when (event.action) { // 对拖拽不同的事件进行处理
DragEvent.ACTION_DROP -> { // ACTION_DROP事件表示拖拽抬手结束的时候
requestDragAndDropPermissions(event) // 申请读取uri的权限
// 处理uri
}
}
true // 返回true代表拖拽事件被处理了
}
拖入方通常有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) // 获取文件后缀名
通过文件后缀名我们就可以准确判断数据类型。