小米MIUI 小部件系统能力说明

小米MIUI 小部件系统能力说明

1.调起 MIUI Widget 商店里的详情页,添加应用的 MIUI Widget

1.1.描述

启动 Widget 详情页,详情页会包含该 App 通过审核的Widget。用户可以左右滑动预览,并选择其中一个添加到桌面。

1.2.调用方式

关键方法
appWidgetManager.requestPinAppWidget(myProvider, extras, null)
使用 extras 携带参数 
addType: appWidgetDetail
widgetName: 小部件name,可选参数,用来指定打开详情页后定位到的组件。如果不填,默认定位到第一个。
widgetExtraData: 用于携带自定义参数,携带自定义参数类型只能是String,最多携带5个,超过5个所有携带的自定义参数都将被抛弃。

注意:该方法仅支持 Android 8.0 及以上系统。不支持 MIUI Widget 的手机调用requestPinAppWidget 方法不会调起 Widget 商店里的详情页。

1.3.示例

public void startAppWidgetPage(Context context){
    AppWidgetManager appWidgetManager =  
    AppWidgetManager.getInstance(context);
    ComponentName myProvider = new ComponentName(this,  
    ExampleWidgetProvider.class);
    if (appWidgetManager.isRequestPinAppWidgetSupported()) {
        Bundle extras = new Bundle();
        extras.putString("addType","appWidgetDetail");
        // packageName 为应用真实包名
⁣        extras.putString("widgetName",
            "packageName/com.miui.ExampleWidgetProvider");
        Bundle dataBundle = new Bundle();
        dataBundle.putString("dataKey1","data1xxx");
        dataBundle.putString("dataKey2","data2xxx");
        dataBundle.putString("dataKey3","data3xxx");
        dataBundle.putString("dataKey4","data4xxx");
        dataBundle.putString("dataKey5","data5xxx");
        extras.putBundle("widgetExtraData", dataBundle);
        appWidgetManager.requestPinAppWidget(myProvider, extras, null);
    }
}

// 获取自定义参数
public class ExampleWidgetProvider extends AppWidgetProvider {
 
   @Override
   public void onAppWidgetOptionsChanged(Context context, AppWidgetManager
     appWidgetManager, int appWidgetId, Bundle newOptions) {
         super.onAppWidgetOptionsChanged(context, appWidgetManager, 
         appWidgetId, newOptions);
        if(extras != null){
            Bundle dataBundle = extras.getBundle("widgetExtraData");
            dataBundle.getString("xxxkey");
        }   
    }    
}

2.设置 MIUI Widget卡片状态

2.1.描述

MIUI Widget 也可以在智能助理(负一屏)进行展示。应用可以选择向系统发送卡片的优先级状态,MIUI智能助理(负一屏)会根据卡片状态进行动态的排序。

2.2.调用方式

key:    miuiWidgetEventCode     
类型:    String
描述:   事件code命名规则请参考code事件表
key:    miuiWidgetTimestamp
类型:   String
描述:   状态变化时的时间戳 
appWidgetManager.updateAppWidgetOptions(widgetId, options);
appWidgetManager.updateAppWidget(widgetId, views);

2.3.code 事件表

事件code命名规则请参考下表,具体事件与code对应关系以双方沟通结果为准
*新增或修改事件需与MIUI商务同学联系,上报未确认的事件code不会生效,恶意错报被系统识别后会降低推荐权重
核心场景事件事件code备注
金融
股票证券
小部件内有股票/基金在交易opening
小部件内无股票/基金在交易closing
出行通勤时间commuting
非通勤时间other
直播/电台小部件内展示内容“直播中”live1不同的事件可用live1、live2…区分
小部件内展示内容“重播中”replay
小部件内展示内容“未开播”other
连接状态已连接connected
未连接disconnect
通知提醒小部件内展示有通知提醒提醒notice1不同的事件可用notice1、notice2…区分
进度状态如“打车进度”“下单进度”等progress1不同的事件可用progress1、progress2…区分
内容资讯如“今日热门”“话题榜”“热搜榜”等info1不同的事件可用info1、info2…区分
功能跳转小部件为纯工具类的功能跳转state1不同的事件可用state1、state2…区分
其他小部件的兜底展示方案或其他状态other

2.4.示例

  • 普通 Widget :在 AppWidgetProvider 的 onUpdate 方法,调用 updateAppWidgetOptions 方法。
public class ExampleWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // todo 准备待更新数据
        ....
        // 更新数据
        for (int appWidgetId : appWidgetIds) {
            RemoteViews views = new RemoteViews(context.getPackageName(), 
            R.layout.widget_ui);
            Bundle options = new Bundle();
            options.putString("miuiWidgetEventCode", "notice1");
            long currentTimeMillis = System.currentTimeMillis();
            options.putString("miuiWidgetTimestamp",
            String.valueOf(currentTimeMillis));
            appWidgetManager.updateAppWidgetOptions(widgetId, options);
            appWidgetManager.updateAppWidget(widgetId, views);
        }
    }
}
  • 列表 Widget : notifyAppWidgetViewDataChanged 更新后调用 updateAppWidget 和updateAppWidgetOptions 方法
public class ExampleWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {
            // todo 准备待更新数据
            ...
            appWidgetManager.notifyAppWidgetViewDataChanged (mAppWidgetId,R.id.content);
            // 更新数据后进行状态更新
            Bundle options = new Bundle();
            options.putString("miuiWidgetEventCode", "notice1");
            long currentTimeMillis = System.currentTimeMillis();
            options.putString("miuiWidgetTimestamp",
            String.valueOf(currentTimeMillis));
            appWidgetManager.updateAppWidgetOptions(mAppWidgetId, options);
            RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(),             R.layout.widget_list_example);
            appWidgetManager.updateAppWidget(mAppWidgetId, remoteViews);
        }
    }
}

注意:updateAppWidgetOptions 需要在 updateAppWidget ()方法前,并且两者需要同一线程。

3.设置编辑页

3.1.描述

MIUI Widget 额外提供进入编辑页的入口。用户点击编辑按钮,可以进入编辑页进行相应的设置。

3.2.调用方法与参数

Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId);
String uriPath = "widgetdemo://com.miui.widgetdemo/widget/WidgetEditActivity";
options.putString("miuiEditUri", uriPath);
appWidgetManager.updateAppWidgetOptions(appWidgetId, options);
appWidgetManager.updateAppWidget(widgetId, views);

3.3.示例

public class ExampleWidgetProvider extends AppWidgetProvider {
    @Override
    public void onUpdate(Context context, AppWidgetManager 
        appWidgetManager, int[] appWidgetIds) {
        for (int appWidgetId : appWidgetIds) {          
            Bundle options =   
            appWidgetManager.getAppWidgetOptions(appWidgetId);
            String uriPath =            "widgetdemo://com.miui.widgetdemo/widget/WidgetEditActivity";
            options.putString("miuiEditUri", uriPath);
            appWidgetManager.updateAppWidgetOptions(appWidgetId, options);
            RemoteViews views =   
            new RemoteViews(context.getPackageName(), 
            R.layout.widget_ui);
            appWidgetManager.updateAppWidget(widgetId, views);
        }
        ....
    }
}
// 通过 intent 可以获取点击的appWidgetId
public class WidgetEditActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Intent intent = getIntent();
        int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
    }
}

4.同功能 MIUI Widget 聚合

4.1.描述

MIUI Widget 可以把不同尺寸相同功能的Widget 在详情页中聚合在一块显示,用户可以根据需求添加相应尺寸的Widget。

4.2.调用方式

在AndroidManifest.xml 文件中 AppWidgetProvider 对应的receiver 如果label 的名称相同的话将会被认为是同一功能的不同尺寸。

4.3.示例

<receiver
    android:name="com.miui.widgetdemo.provider.ExampleWidgetProvider1"
    android:label="@string/app_widget_group">
    ...
</receiver>

<receiver
    android:name="com.miui.widgetdemo.provider.ExampleWidgetProvider2"
    android:label="@string/app_widget_group">
   ...
</receiver>

5.点击  MIUI Widget 跳转应用页面

5.1.描述

在 AppWidgetProvider 的onUpdate方法中,使用 RemoteViews 的 setOnClickPendingIntent 设置启动的 Intent 。使用 AppWidgetManager 的 updateAppWidget方法关联RemoteViews 和Widget。

5.2.参数

// viewId Widget 布局 id
// PendingIntent Intent的封装
// appWidgetId Widget 的 id,可在 onUpdate 方法参数中获取
// appWidgetManager widget 的管理对象,可在 onUpdate 方法参数获取
remoteViews.setOnClickPendingIntent(int viewId, PendingIntent pendingIntent)
appWidgetManager.updateAppWidget(int appWidgetId, RemoteViews views)

5.3.示例

// step1: 构建跳转页面的 PendingIntent
Intent intent = new Intent(context, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// step2: 生成 RemoteViews 关联 PendingIntent
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views.setOnClickPendingIntent(R.id.button, pendingIntent);
// step3: 关联 widget 和 RemoteViews
appWidgetManager.updateAppWidget(appWidgetId, views);

6.判断 MIUI Widget 是否已经添加

6.1.描述

使用系统 AppWidgetManager 的 getAppWidgetIds方法判断某个Widget是否已添加。

6.2.示例

ComponentName componentName = new ComponentName(getPackageName(), "com.miui.ExampleAppWidgetProvider");
int[] appWidgetIds = AppWidgetManager.getInstance(getApplicationContext()).getAppWidgetIds(componentName);
if(appWidgetIds.length > 0){
    // 已添加
} else {
    // 未添加
}

7.判断是否支持MIUI Widget

7.1.描述

开发者可以通过示例方式判断当前手机是否支持MIUI Widget。

7.2.示例

@WorkerThread
public boolean isMiuiWidgetSupported() {
    Uri uri = 
    Uri.parse("content://com.miui.personalassistant.widget.external");
    boolean isMiuiWidgetSupported = false;
    try {
        Bundle bundle = getContentResolver().call(uri, 
        "isMiuiWidgetSupported", null, null);
        if (bundle != null) {
            isMiuiWidgetSupported = bundle.getBoolean("isMiuiWidgetSupported");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return isMiuiWidgetSupported;
}

8.判断是否支持MIUI Widget 小部件详情页

8.1.描述

开发者可以通过示例方式判断当前手机是否支持MIUI Widget 详情页。为了提升用户体验,部分机型支持 MIUI Widget(包含MIUI Widget 特性,例如曝光刷新等), 但不支持调起MIUI Widget 详情页。这部分机型添加小部件的方式与旧版系统一致。

8.2.示例

@WorkerThread
public boolean isMiuiWidgetDetailPageSupported() {
    Uri uri = 
    Uri.parse("content://com.miui.personalassistant.widget.external");
    boolean isMiuiWidgetDetailPageSupported = false;
    try {
        Bundle bundle = getContentResolver().call(uri, 
        "isMiuiWidgetDetailPageSupported", null, null);
        if (bundle != null) {
            isMiuiWidgetDetailPageSupported = bundle.getBoolean("isMiuiWidgetDetailPageSupported");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return isMiuiWidgetDetailPageSupported;
}

9.MIUI Widget 与 Activity 切换动画

9.1.描述

MIUI 系统为MIUI Widget 和 Activity 切换时增加了过渡动画。开发者可根据自身业务场景决定是否使用过渡动画。动画默认开启,如果不使用,设置”miuiWidgetTransitionAnimation”为false。

9.2.示例

<receiver android:name="com.miui.ExampleWidgetProvider" >
    <meta-data
    android:name="miuiWidget"
    android:value="true" />
    // 关闭动画
    <meta-data
    android:name="miuiWidgetTransitionAnimation"
    android:value="false" />

    ...
</receiver>

10.App主动刷新小部件

10.1.描述

当应用在前台使用或在后台存活时,可调用AppWidgetManager. updateAppWidget()方法,主动刷新小部件。这样可以提高小部件内容的实时性和准确性。

10.2.示例

// 通过组件名更新
public void updateWidget(Context context) {
    // R.layout.demo_appwidget_normal_example 为小部件布局文件,这里仅示例
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
            R.layout.demo_appwidget_normal_example);
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    // NormalExampleWidgetProvider 为小部件组件名字,这里仅示例
    ComponentName componentName = new ComponentName(context, NormalExampleWidgetProvider.class);
    appWidgetManager.updateAppWidget(componentName, remoteViews);
}
// 通过widgetId更新
public void updateWidgetByWidgetId(Context context, int widgetId) {
    // R.layout.demo_appwidget_normal_example 为小部件布局文件,这里仅示例
    RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
            R.layout.demo_appwidget_normal_example);
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    appWidgetManager.updateAppWidget(widgetId, remoteViews);
}

11.Push透传刷新服务

11.1.描述

当MIUI Widget状态发生改变且Widget不可见、主应用未启动时,开发者可调用小部件服务端提供的更新API,经由MI PUSH发送透传消息给负一屏/桌面客户端,由负一屏/桌面拉起Widget独立进程(不唤醒应用主进程),以实现MIUI Widget实时刷新的目的。

流程图:

11.2.接口规范

域名: https://developer.assistant.miui.com
路径: /openapi/widget/{cpCode}/refresh
     cpCode值由小部件开发人员提供
参数:需包含Header以及Body
Header参数表
头域名称描述是否必选类型取值范围
Content-Type固定值,填application/jsonStringapplication/json
app-id在小米开放平台注册的程序编号String
access-token应用级token(校验请求权限,通过帐号服务获取)String最大长度:259
sign签名(验证请求完整性,下方有签名生成方法)String
timestamp当前时间戳(防止请求重放,会根据该字段判断请求有效期)long13位 毫秒时间戳
trace-id请求的唯一标识(用来定位每次请求)String只能包含数字和大小写字母,最大长度64
Body参数表
字段名描述必须类型
oaid设备oaidString
widgetId设备 widgetId (安卓生成的id)String
widgetProviderName小部件providerNameString
extra额外透传到小部件内容String

注意:

  • 获取access-token:access-token的获取方式请联系相关技术支持。
  • 处理签名
签名生成: 
    假设: appId = "111111111" , 当前时间戳是 1606206667013 ,秘钥是 "testSecret"
    step 1: 先将body进行md5,再转化为16进制小写字符串
            公式:md5Body= Lowercase(hexEncode(md5(body)))
    step 2: 将appId、毫秒时间戳和body的md5Body值,按照参数名称进行字典排序,用‘&’连接获得strToDigest
            如:strToDigest="appId=111111111&body=md5Body&timestamp=1606206667013"          
    step 3: 将获得到的摘要字符串(strToDigest)进行HMAC_SHA_256,再转化为16进制小写字符串得到签名
            公式:Lowercase(hexEncode(HMAC_SHA_256("testSecret", strToDigest)))
// 签名java demo: 
// demo引用了apache的工具类进行处理,若不想引用可以参考其逻辑自行实现
<!-- https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.14</version>
</dependency>

@Slf4j
public class SignatureUtils {
    
    /**
     * 生成签名
     *
     * @param appId     应用id
     * @param secret    秘钥
     * @param timestamp 请求的时间戳
     * @param body      请求的body内容
     * @return 签名信息
     */
    public static String generateSignature(String appId, String secret, String timestamp, String body) {
        String sign = null;
        try {
            String md5Body = DigestUtils.md5Hex(body);
            Map<String, String> paramMap = new TreeMap<>();
            paramMap.put("appId", appId);
            paramMap.put("timestamp", timestamp);
            paramMap.put("body", md5Body);
            String strToDigest = paramMap.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
            sign = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret).hmacHex(strToDigest);
        } catch (Exception e) {
            log.error("generate signature exception !", e);
        }
        return sign;
    }
    
    /**
     * 验证签名
     *
     * @param appId     应用id
     * @param secret    秘钥
     * @param timestamp 请求的时间戳
     * @param body      请求的body内容
     * @param sign      请求的签名
     * @return 签名信息
     */
    public static boolean verifySignature(String appId, String secret, String timestamp, String body, String sign) {
        return sign.equals(generateSignature(appId, secret, timestamp, body));
    }
}
  • 返回状态码定义
状态码描述
成功0
服务异常1000
无效参数1001
缺失参数1002
token验证失败3000
签名验证失败4000
流量限制5000
  • 开发者MIUI Widget可从刷新广播的Intent中得到extra信息。

11.3.示例

// Curl示例:
curl --location --request POST 'http://developer.assistant.miui.com/openapi/widget/testCp/refresh' 
    --header 'app-id: 111111111' 
    --header 'access-token: A1_oKs19iiWArgij6qFYaEAooAMqG3bhXUgS0MZQf63KQTgWju-oj9YuccqR1EhbugrqnFmooNr6mKdkfEdKN740fUYpxk0o9ZHE5QpFvR1fhaoJ7xELYD1byNnYZb-tB-y5DPXRLIp8ikod5lUZGnayuLVPePa7uB1LlVzw-qPS-U' 
    --header 'sign: 664d528f398af20f23b6e4ec43d4e9662476ee8fc9c5cee42dd897b1af0152e7' 
    --header 'timestamp: 1636341480000' 
    --header 'trace-id: 123123' 
    --header 'Content-Type: application/json' 
    --data-raw '{
        "oaid": "f29eb7d12222fb6b",
        "widgetId": "666",
        "widgetProviderName": "com.miui.test",
        "extra": "{"test":123}"
    }'