Android消息推送:手把手教你集成小米推送

前言

  • 在Android開發(fā)中,消息推送功能的使用非常常見。
推送消息截圖
  • 為了降低開發(fā)成本,使用第三方推送是現(xiàn)今較為流行的解決方案。
  • 今天,我將手把手教大家如何在你的應(yīng)用里集成小米推送
  1. 該文檔基于小米推送官方Demo進(jìn)行解析,并給出簡易推送Demo
  2. 看該文檔前,請先閱讀我寫的另外兩篇文章:
    史上最全解析Android消息推送解決方案
    Android推送:第三方消息推送平臺詳細(xì)解析

目錄

目錄

1. 官方Demo解析

首先,我們先對小米官方的推送Demo進(jìn)行解析。

請先到官網(wǎng)下載官方DemoSDK說明文檔

1.1 Demo概況

Demo目錄

目錄說明:

  • DemoApplication類
    繼承自Application類,其作用主要是:設(shè)置App的ID & Key、注冊推送服務(wù)

  • DemoMessageReceiver類
    繼承自BroadcastReceiver,用于接收推送消息并對這些消息進(jìn)行處理

  • MainActivity
    實現(xiàn)界面按鈕處理 & 設(shè)置本地推送方案

  • TimeIntervalDialog
    設(shè)置推送的時間間段

接下來,我將對每個類進(jìn)行詳細(xì)分析

1.2 詳細(xì)分析

1.2.1 DemoApplication類

繼承自Application類,其作用主要是:

  • 設(shè)置App的ID & Key
  • 注冊推送服務(wù)

接下來我們通過代碼來看下這兩個功能如何實現(xiàn):

DemoApplication.java

package com.xiaomi.mipushdemo;

import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.Application;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import com.xiaomi.channel.commonutils.logger.LoggerInterface;
import com.xiaomi.mipush.sdk.Logger;
import com.xiaomi.mipush.sdk.MiPushClient;

import java.util.List;


public class DemoApplication extends Application {

    // 使用自己APP的ID(官網(wǎng)注冊的)
    private static final String APP_ID = "1000270";
    // 使用自己APP的KEY(官網(wǎng)注冊的)
    private static final String APP_KEY = "670100056270";

    // 此TAG在adb logcat中檢索自己所需要的信息, 只需在命令行終端輸入 adb logcat | grep
    // com.xiaomi.mipushdemo
    public static final String TAG = "com.xiaomi.mipushdemo";

    private static DemoHandler sHandler = null;
    private static MainActivity sMainActivity = null;

    //為了提高推送服務(wù)的注冊率,官方Demo建議在Application的onCreate中初始化推送服務(wù)
    //你也可以根據(jù)需要,在其他地方初始化推送服務(wù)
    
    @Override
    public void onCreate() {

        super.onCreate();
        
        //判斷用戶是否已經(jīng)打開App,詳細(xì)見下面方法定義
        if (shouldInit()) {
        //注冊推送服務(wù)
        //注冊成功后會向DemoMessageReceiver發(fā)送廣播
        // 可以從DemoMessageReceiver的onCommandResult方法中MiPushCommandMessage對象參數(shù)中獲取注冊信息
            MiPushClient.registerPush(this, APP_ID, APP_KEY);
         //參數(shù)說明
        //context:Android平臺上app的上下文,建議傳入當(dāng)前app的application context
        //appID:在開發(fā)者網(wǎng)站上注冊時生成的,MiPush推送服務(wù)頒發(fā)給app的唯一認(rèn)證標(biāo)識
       //appKey:在開發(fā)者網(wǎng)站上注冊時生成的,與appID相對應(yīng),用于驗證appID是否合法
        }


        //下面是與測試相關(guān)的日志設(shè)置
        LoggerInterface newLogger = new LoggerInterface() {

            @Override
            public void setTag(String tag) {
                // ignore
            }

            @Override
            public void log(String content, Throwable t) {
                Log.d(TAG, content, t);
            }

            @Override
            public void log(String content) {
                Log.d(TAG, content);
            }
        };
        Logger.setLogger(this, newLogger);
        if (sHandler == null) {
            sHandler = new DemoHandler(getApplicationContext());
        }
    }


//通過判斷手機(jī)里的所有進(jìn)程是否有這個App的進(jìn)程
//從而判斷該App是否有打開
    private boolean shouldInit() {
//通過ActivityManager我們可以獲得系統(tǒng)里正在運行的activities
//包括進(jìn)程(Process)等、應(yīng)用程序/包、服務(wù)(Service)、任務(wù)(Task)信息。
        ActivityManager am = ((ActivityManager) getSystemService(Context.ACTIVITY_SERVICE));
        List<RunningAppProcessInfo> processInfos = am.getRunningAppProcesses();
        String mainProcessName = getPackageName();
        
       //獲取本App的唯一標(biāo)識
        int myPid = Process.myPid();
        //利用一個增強(qiáng)for循環(huán)取出手機(jī)里的所有進(jìn)程
        for (RunningAppProcessInfo info : processInfos) {
            //通過比較進(jìn)程的唯一標(biāo)識和包名判斷進(jìn)程里是否存在該App
            if (info.pid == myPid && mainProcessName.equals(info.processName)) {
                return true;
            }
        }
        return false;
    }

    public static DemoHandler getHandler() {
        return sHandler;
    }

    public static void setMainActivity(MainActivity activity) {
        sMainActivity = activity;
    }


//通過設(shè)置Handler來設(shè)置提示文案
    public static class DemoHandler extends Handler {

        private Context context;

        public DemoHandler(Context context) {
            this.context = context;
        }

        @Override
        public void handleMessage(Message msg) {
            String s = (String) msg.obj;
            if (sMainActivity != null) {
                sMainActivity.refreshLogInfo();
            }
            if (!TextUtils.isEmpty(s)) {
                Toast.makeText(context, s, Toast.LENGTH_LONG).show();
            }
        }
    }
}

總結(jié):

  • 步驟1:先判斷應(yīng)用App是否已開啟 - 通過判斷系統(tǒng)里的進(jìn)程
  • 通過靜態(tài)方法
public static void registerPush(Context context, String appID, String appKey)

進(jìn)行推送服務(wù)注冊,詳細(xì)參數(shù)如下:

  • 為了提高注冊率,最好在Application的onCreate中初始化推送服務(wù)

你也可以根據(jù)需要,在其他地方初始化推送服務(wù)

1.2.2 DemoMessageReceiver類

繼承自PushMessageReceiver(抽象類,繼承自BroadcastReceiver),其作用主要是:

  • 接收推送消息
  • 對推送消息進(jìn)行處理

DemoMessageReceiver.java

package com.xiaomi.mipushdemo;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;

import com.xiaomi.mipush.sdk.ErrorCode;
import com.xiaomi.mipush.sdk.MiPushClient;
import com.xiaomi.mipush.sdk.MiPushCommandMessage;
import com.xiaomi.mipush.sdk.MiPushMessage;
import com.xiaomi.mipush.sdk.PushMessageReceiver;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

/**
 * 1、PushMessageReceiver 是個抽象類,該類繼承了 BroadcastReceiver。
 * 2、需要將自定義的 DemoMessageReceiver 注冊在 AndroidManifest.xml



public class DemoMessageReceiver extends PushMessageReceiver {

    private String mRegId;
    private String mTopic;
    private String mAlias;
    private String mAccount;
    private String mStartTime;
    private String mEndTime;


    //透傳消息到達(dá)客戶端時調(diào)用
    //作用:可通過參數(shù)message從而獲得透傳消息,具體請看官方SDK文檔
    @Override
    public void onReceivePassThroughMessage(Context context, MiPushMessage message) {
        Log.v(DemoApplication.TAG,
                "onReceivePassThroughMessage is called. " + message.toString());
        String log = context.getString(R.string.recv_passthrough_message, message.getContent());
        MainActivity.logList.add(0, getSimpleDate() + " " + log);

        if (!TextUtils.isEmpty(message.getTopic())) {
            mTopic = message.getTopic();
        } else if (!TextUtils.isEmpty(message.getAlias())) {
            mAlias = message.getAlias();
        }

        Message msg = Message.obtain();
        msg.obj = log;
        DemoApplication.getHandler().sendMessage(msg);
    }


//通知消息到達(dá)客戶端時調(diào)用
     //注:應(yīng)用在前臺時不彈出通知的通知消息到達(dá)客戶端時也會回調(diào)函數(shù)
    //作用:通過參數(shù)message從而獲得通知消息,具體請看官方SDK文檔
   
    @Override
    public void onNotificationMessageArrived(Context context, MiPushMessage message) {
        Log.v(DemoApplication.TAG,
                "onNotificationMessageArrived is called. " + message.toString());
        String log = context.getString(R.string.arrive_notification_message, message.getContent());
        MainActivity.logList.add(0, getSimpleDate() + " " + log);

        if (!TextUtils.isEmpty(message.getTopic())) {
            mTopic = message.getTopic();
        } else if (!TextUtils.isEmpty(message.getAlias())) {
            mAlias = message.getAlias();
        }

        Message msg = Message.obtain();
        msg.obj = log;
        DemoApplication.getHandler().sendMessage(msg);
    }
    
    //用戶手動點擊通知欄消息時調(diào)用
     //注:應(yīng)用在前臺時不彈出通知的通知消息到達(dá)客戶端時也會回調(diào)函數(shù)
    //作用:1. 通過參數(shù)message從而獲得通知消息,具體請看官方SDK文檔
    //2. 設(shè)置用戶點擊消息后打開應(yīng)用 or 網(wǎng)頁 or 其他頁面

    @Override
    public void onNotificationMessageClicked(Context context, MiPushMessage message) {
        Log.v(DemoApplication.TAG,
                "onNotificationMessageClicked is called. " + message.toString());
        String log = context.getString(R.string.click_notification_message, message.getContent());
        MainActivity.logList.add(0, getSimpleDate() + " " + log);

        if (!TextUtils.isEmpty(message.getTopic())) {
            mTopic = message.getTopic();
        } else if (!TextUtils.isEmpty(message.getAlias())) {
            mAlias = message.getAlias();
        }

        Message msg = Message.obtain();
        if (message.isNotified()) {
            msg.obj = log;
        }
        DemoApplication.getHandler().sendMessage(msg);
    }


    
    //用來接收客戶端向服務(wù)器發(fā)送命令后的響應(yīng)結(jié)果。
    @Override
    public void onCommandResult(Context context, MiPushCommandMessage message) {
        Log.v(DemoApplication.TAG,
                "onCommandResult is called. " + message.toString());
        String command = message.getCommand();
        List<String> arguments = message.getCommandArguments();
        String cmdArg1 = ((arguments != null && arguments.size() > 0) ? arguments.get(0) : null);
        String cmdArg2 = ((arguments != null && arguments.size() > 1) ? arguments.get(1) : null);
        String log;
        if (MiPushClient.COMMAND_REGISTER.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mRegId = cmdArg1;
                log = context.getString(R.string.register_success);

            } else {
                log = context.getString(R.string.register_fail);
            }
        } else if (MiPushClient.COMMAND_SET_ALIAS.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mAlias = cmdArg1;
                log = context.getString(R.string.set_alias_success, mAlias);
            } else {
                log = context.getString(R.string.set_alias_fail, message.getReason());
            }
        } else if (MiPushClient.COMMAND_UNSET_ALIAS.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mAlias = cmdArg1;
                log = context.getString(R.string.unset_alias_success, mAlias);
            } else {
                log = context.getString(R.string.unset_alias_fail, message.getReason());
            }
        } else if (MiPushClient.COMMAND_SET_ACCOUNT.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mAccount = cmdArg1;
                log = context.getString(R.string.set_account_success, mAccount);
            } else {
                log = context.getString(R.string.set_account_fail, message.getReason());
            }
        } else if (MiPushClient.COMMAND_UNSET_ACCOUNT.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mAccount = cmdArg1;
                log = context.getString(R.string.unset_account_success, mAccount);
            } else {
                log = context.getString(R.string.unset_account_fail, message.getReason());
            }
        } else if (MiPushClient.COMMAND_SUBSCRIBE_TOPIC.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mTopic = cmdArg1;
                log = context.getString(R.string.subscribe_topic_success, mTopic);
            } else {
                log = context.getString(R.string.subscribe_topic_fail, message.getReason());
            }
        } else if (MiPushClient.COMMAND_UNSUBSCRIBE_TOPIC.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mTopic = cmdArg1;
                log = context.getString(R.string.unsubscribe_topic_success, mTopic);
            } else {
                log = context.getString(R.string.unsubscribe_topic_fail, message.getReason());
            }
        } else if (MiPushClient.COMMAND_SET_ACCEPT_TIME.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mStartTime = cmdArg1;
                mEndTime = cmdArg2;
                log = context.getString(R.string.set_accept_time_success, mStartTime, mEndTime);
            } else {
                log = context.getString(R.string.set_accept_time_fail, message.getReason());
            }
        } else {
            log = message.getReason();
        }
        MainActivity.logList.add(0, getSimpleDate() + "    " + log);

        Message msg = Message.obtain();
        msg.obj = log;
        DemoApplication.getHandler().sendMessage(msg);
    }


    //用于接收客戶端向服務(wù)器發(fā)送注冊命令后的響應(yīng)結(jié)果。
    @Override
    public void onReceiveRegisterResult(Context context, MiPushCommandMessage message) {
        Log.v(DemoApplication.TAG,
                "onReceiveRegisterResult is called. " + message.toString());
        String command = message.getCommand();
        List<String> arguments = message.getCommandArguments();
        String cmdArg1 = ((arguments != null && arguments.size() > 0) ? arguments.get(0) : null);
        String log;
        if (MiPushClient.COMMAND_REGISTER.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                mRegId = cmdArg1;
                //打印日志:注冊成功
                log = context.getString(R.string.register_success);
            } else {
                      //打印日志:注冊失敗
                log = context.getString(R.string.register_fail);
            }
        } else {
            log = message.getReason();
        }

        Message msg = Message.obtain();
        msg.obj = log;
        DemoApplication.getHandler().sendMessage(msg);
    }

    @SuppressLint("SimpleDateFormat")
    private static String getSimpleDate() {
        return new SimpleDateFormat("MM-dd hh:mm:ss").format(new Date());
    }

}

總結(jié)

  • 根據(jù)需要復(fù)寫PushMessageReceiver里對消息的相關(guān)處理方法,以下是相關(guān)方法的詳情:


    相關(guān)方法詳情
  • 關(guān)于onCommandResult(Context context,MiPushCommandMessage message)
    a. 作用:當(dāng)客戶端向服務(wù)器發(fā)送注冊push、設(shè)置alias、取消注冊alias、訂閱topic、取消訂閱topic等等命令后,從服務(wù)器返回結(jié)果。
    b. 參數(shù)說明

    參數(shù)說明

1.2.3 MainActivity

用于給用戶設(shè)置標(biāo)識,如別名、標(biāo)簽、賬號等等

MainActivity.java

public class MainActivity extends Activity {

    public static List<String> logList = new CopyOnWriteArrayList<String>();

    private TextView mLogView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DemoApplication.setMainActivity(this);

        mLogView = (TextView) findViewById(R.id.log);
        
        // 設(shè)置別名
        findViewById(R.id.set_alias).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                final EditText editText = new EditText(MainActivity.this);
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle(R.string.set_alias)
                        .setView(editText)
                        .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {

                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                String alias = editText.getText().toString();
//調(diào)用靜態(tài)方法進(jìn)行設(shè)置                                MiPushClient.setAlias(MainActivity.this, alias, null);
                            }

                        })
                        .setNegativeButton(R.string.cancel, null)
                        .show();
            }
        });
        // 撤銷別名
        findViewById(R.id.unset_alias).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                final EditText editText = new EditText(MainActivity.this);
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle(R.string.unset_alias)
                        .setView(editText)
                        .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {

                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                String alias = editText.getText().toString();
//調(diào)用靜態(tài)方法進(jìn)行設(shè)置                                  MiPushClient.unsetAlias(MainActivity.this, alias, null);
                            }

                        })
                        .setNegativeButton(R.string.cancel, null)
                        .show();

            }
        });
        // 設(shè)置帳號
        findViewById(R.id.set_account).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                final EditText editText = new EditText(MainActivity.this);
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle(R.string.set_account)
                        .setView(editText)
                        .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {

                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                String account = editText.getText().toString();
//調(diào)用靜態(tài)方法進(jìn)行設(shè)置                                  MiPushClient.setUserAccount(MainActivity.this, account, null);
                            }

                        })
                        .setNegativeButton(R.string.cancel, null)
                        .show();

            }
        });
        // 撤銷帳號
        findViewById(R.id.unset_account).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                final EditText editText = new EditText(MainActivity.this);
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle(R.string.unset_account)
                        .setView(editText)
                        .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {

                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                String account = editText.getText().toString();
//調(diào)用靜態(tài)方法進(jìn)行設(shè)置                                  MiPushClient.unsetUserAccount(MainActivity.this, account, null);
                            }

                        })
                        .setNegativeButton(R.string.cancel, null)
                        .show();
            }
        });
        // 設(shè)置標(biāo)簽
        findViewById(R.id.subscribe_topic).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                final EditText editText = new EditText(MainActivity.this);
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle(R.string.subscribe_topic)
                        .setView(editText)
                        .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {

                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                String topic = editText.getText().toString();
//調(diào)用靜態(tài)方法進(jìn)行設(shè)置                                  MiPushClient.subscribe(MainActivity.this, topic, null);
                            }

                        })
                        .setNegativeButton(R.string.cancel, null)
                        .show();
            }
        });
        // 撤銷標(biāo)簽
        findViewById(R.id.unsubscribe_topic).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                final EditText editText = new EditText(MainActivity.this);
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle(R.string.unsubscribe_topic)
                        .setView(editText)
                        .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {

                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                String topic = editText.getText().toString();
//調(diào)用靜態(tài)方法進(jìn)行設(shè)置                                  MiPushClient.unsubscribe(MainActivity.this, topic, null);
                            }

                        })
                        .setNegativeButton(R.string.cancel, null)
                        .show();
            }
        });
        // 設(shè)置接收消息時間
        findViewById(R.id.set_accept_time).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                new TimeIntervalDialog(MainActivity.this, new TimeIntervalInterface() {

                    @Override
                    public void apply(int startHour, int startMin, int endHour,
                                      int endMin) {
                        //調(diào)用靜態(tài)方法進(jìn)行設(shè)置  
                        MiPushClient.setAcceptTime(MainActivity.this, startHour, startMin, endHour, endMin, null);
                    }

                    @Override
                    public void cancel() {
                        //ignore
                    }

                })
                        .show();
            }
        });
        // 暫停推送
        findViewById(R.id.pause_push).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                MiPushClient.pausePush(MainActivity.this, null);
            }
        });

        findViewById(R.id.resume_push).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
            //調(diào)用靜態(tài)方法進(jìn)行設(shè)置  
                MiPushClient.resumePush(MainActivity.this, null);
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        refreshLogInfo();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        DemoApplication.setMainActivity(null);
    }

    public void refreshLogInfo() {
        String AllLog = "";
        for (String log : logList) {
            AllLog = AllLog + log + "\n\n";
        }
        mLogView.setText(AllLog);
    }
}

總結(jié)

根據(jù)需求對不同用戶設(shè)置不同的推送標(biāo)識,如別名、標(biāo)簽等等。

a. 別名(Alias)

  • 開發(fā)者可以為指定用戶設(shè)置別名,然后給這個別名推送消息,

效果等同于給RegId推送消息,Alias是除Regid(自動生成的)和UserAccount之外的第三個用戶標(biāo)識

  • 開發(fā)者可以取消指定用戶的某個別名,服務(wù)器就不會給這個別名推送消息了。
//設(shè)置別名
MiPushClient.setAlias(Context context, String alias, String category);

//撤銷別名
MiPushClient.unsetAlias(Context context, String alias, String category);
//參數(shù)說明
//context:Android平臺上app的上下文,建議傳入當(dāng)前app的application context
//alias:為指定用戶設(shè)置別名 / 為指定用戶取消別名
//category:擴(kuò)展參數(shù),暫時沒有用途,直接填null

//獲取該客戶端所有的別名
public static List<String> getAllAlias(final Context context)

b. 用戶賬號(UserAccoun)

  • 開發(fā)者可以為指定用戶設(shè)置userAccount
  • 開發(fā)者可以取消指定用戶的某個userAccount,服務(wù)器就不會給這個userAccount推送消息了
//設(shè)置
MiPushClient.setUserAccount(final Context context, final String userAccount, String
category)

//撤銷
MiPushClient.unsetUserAccount(final Context context, final String userAccount, String
category)
//參數(shù)說明
//context:Android平臺上app的上下文,建議傳入當(dāng)前app的application context
//userAccount:為指定用戶設(shè)置userAccount / 為指定用戶取消userAccount
//category:擴(kuò)展參數(shù),暫時沒有用途,直接填null

//獲取該客戶端所有設(shè)置的賬號
public static List<String> getAllUserAccount(final Context context)

c. 標(biāo)簽(Topic)

  • 開發(fā)者可以結(jié)合自己的業(yè)務(wù)特征,給用戶打上不同的標(biāo)簽。
  • 消息推送時,開發(fā)者可以結(jié)合每條消息的內(nèi)容和目標(biāo)用戶,為每條消息選擇對應(yīng)的標(biāo)簽,為開發(fā)者可以根據(jù)訂閱的主題實現(xiàn)分組群發(fā),從而進(jìn)行消息的精準(zhǔn)推送
//設(shè)置標(biāo)簽
MiPushClient.subscribe(Context context, String topic, String category);
//撤銷標(biāo)簽
MiPushClient.unsubscribe(Context context, String topic, String category);
//參數(shù)說明
//context:Android平臺上app的上下文,建議傳入當(dāng)前app的application context
//topic:為指定用戶設(shè)置設(shè)置訂閱的主題 / 為指定用戶取消訂閱的主題
//category:擴(kuò)展參數(shù),暫時沒有用途,直接填null

//獲取該客戶端所有的標(biāo)簽
public static List<String> getAllTopic(final Context context);

TimeIntervalDialog

作用:用于設(shè)置推送的時間-開始時間+暫停時間

package com.xiaomi.mipushdemo;

import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TimePicker;
import android.widget.TimePicker.OnTimeChangedListener;

//繼承OnTimeChangedListener接口
public class TimeIntervalDialog extends Dialog implements OnTimeChangedListener {

    
    private TimeIntervalInterface mTimeIntervalInterface;
    private Context mContext;
    private TimePicker mStartTimePicker, mEndTimePicker;
    private int mStartHour, mStartMinute, mEndHour, mEndMinute;

    private Button.OnClickListener clickListener = new Button.OnClickListener() {

        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.apply:
                    dismiss();
                    //設(shè)置時間參數(shù)
                    mTimeIntervalInterface.apply(mStartHour, mStartMinute, mEndHour, mEndMinute);
                    break;
                case R.id.cancel:
                    dismiss();
                    mTimeIntervalInterface.cancel();
                    break;
                default:
                    break;
            }
        }
    };

    public TimeIntervalDialog(Context context, TimeIntervalInterface timeIntervalInterface,
                              int startHour, int startMinute, int endHour, int endMinute) {
        super(context);
        mContext = context;
        this.mTimeIntervalInterface = timeIntervalInterface;
        this.mStartHour = startHour;
        this.mStartMinute = startMinute;
        this.mEndHour = endHour;
        this.mEndMinute = endMinute;
    }

    public TimeIntervalDialog(Context context, TimeIntervalInterface timeIntervalInterface) {
        this(context, timeIntervalInterface, 0, 0, 23, 59);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.set_time_dialog);
        setCancelable(true);
        setTitle(mContext.getString(R.string.set_accept_time));
        mStartTimePicker = (TimePicker) findViewById(R.id.startTimePicker);
        mStartTimePicker.setIs24HourView(true);
        mStartTimePicker.setCurrentHour(mStartHour);
        mStartTimePicker.setCurrentMinute(mStartMinute);
        mStartTimePicker.setOnTimeChangedListener(this);
        mEndTimePicker = (TimePicker) findViewById(R.id.endTimePicker);
        mEndTimePicker.setIs24HourView(true);
        mEndTimePicker.setCurrentHour(mEndHour);
        mEndTimePicker.setCurrentMinute(mEndMinute);
        mEndTimePicker.setOnTimeChangedListener(this);
        Button applyBtn = (Button) findViewById(R.id.apply);
        applyBtn.setOnClickListener(clickListener);
        Button cancelBtn = (Button) findViewById(R.id.cancel);
        cancelBtn.setOnClickListener(clickListener);
    }

    @Override
    public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
        if (view == mStartTimePicker) {
            mStartHour = hourOfDay;
            mStartMinute = minute;
        } else if (view == mEndTimePicker) {
            mEndHour = hourOfDay;
            mEndMinute = minute;
        }
    }

    interface TimeIntervalInterface {
        void apply(int startHour, int startMin, int endHour, int endMin);

        void cancel();
    }
}

總結(jié)

  • 使用一個繼承了Dialog類的TimeIntervalDialog類進(jìn)行推送時間的配置
  • 可進(jìn)行的配置:設(shè)置推送時間(開始 & 結(jié)束)、暫停推送時間、恢復(fù)推送時間
//設(shè)置推送時間(開始 & 結(jié)束)
MiPushClient.setAcceptTime(Context context, int startHour, int startMin, int endHour,
int endMin, String category)
//設(shè)置暫停推送時間、恢復(fù)推送時間
pausePush(Context context, String category)`和`resumePush(Context context, String category)
//參數(shù)說明
//context:Android平臺上app的上下文,建議傳入當(dāng)前app的application context
//startHour:接收時段開始時間的小時
//startMin  :接收時段開始時間的分鐘
//endHour:接收時段結(jié)束時間的小時
//endMin:接收時段結(jié)束時間的分鐘
//category:擴(kuò)展參數(shù),暫時沒有用途,直接填null

AndroidManifest文件的配置

//小米推送支持最低的Android版本是2.2
<uses-sdk  android:minSdkVersion="8"/>

//設(shè)置一系列權(quán)限
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.VIBRATE" />

//這里com.xiaomi.mipushdemo改成自身app的包名
    <permission android:name="com.xiaomi.mipushdemo.permission.MIPUSH_RECEIVE" android:protectionLevel="signature" />

//這里com.xiaomi.mipushdemo改成自身app的包名
    <uses-permission android:name="com.xiaomi.mipushdemo.permission.MIPUSH_RECEIVE" />


//注冊廣播BroadcastReceiver & Service
//都是靜態(tài)注冊,因為要長期處在后臺運行
//注:共是3個廣播接收器和4個服務(wù),其中包括繼承了PushMessageReceiver的DemoMessageReceiver
                
        //4個后臺服務(wù)
        <service
          android:enabled="true"
          android:process=":pushservice"
          android:name="com.xiaomi.push.service.XMPushService"/>

        //此service必須在3.0.1版本以后(包括3.0.1版本)加入
        <service
          android:name="com.xiaomi.push.service.XMJobService"
          android:enabled="true"
          android:exported="false"
          android:permission="android.permission.BIND_JOB_SERVICE"
          android:process=":pushservice" />
        
        //此service必須在2.2.5版本以后(包括2.2.5版本)加入
        <service
          android:enabled="true"
          android:exported="true"
          android:name="com.xiaomi.mipush.sdk.PushMessageHandler" /> 

        <service android:enabled="true"
          android:name="com.xiaomi.mipush.sdk.MessageHandleService" /> 
        

        //3個廣播
        <receiver
          android:exported="true"
          android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver" >
          <intent-filter>
            <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
            <category android:name="android.intent.category.DEFAULT" />
          </intent-filter>
        </receiver>

        <receiver
          android:exported="false"
          android:process=":pushservice"
          android:name="com.xiaomi.push.service.receivers.PingReceiver" >
          <intent-filter>
            <action android:name="com.xiaomi.push.PING_TIMER" />
          </intent-filter>
        </receiver>

//繼承了PushMessageReceiver的DemoMessageReceiver的廣播注冊
        <receiver
            android:name="com.xiaomi.mipushdemo.DemoMessageReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED" />
            </intent-filter>
            <intent-filter>
                <action android:name="com.xiaomi.mipush.ERROR" />
            </intent-filter>
        </receiver>

2. 集成小米推送步驟匯總

  • 步驟1:在小米推送平臺進(jìn)行相關(guān)注冊開發(fā)者賬號,并進(jìn)行應(yīng)用的注冊:應(yīng)用包名,AppID和AppKey
  • 步驟2:將小米推送的SDK包加入庫
  • 步驟3:在應(yīng)用內(nèi)初始化小米推送服務(wù)
  • 步驟4:繼承PushMessageReceiver,并復(fù)寫相關(guān)推送消息的方法
  • 步驟5:在AndroidManifest文件里面配置好權(quán)限、注冊Service和BroadcastReceiver

在Android6.0里面的權(quán)限需要動態(tài)獲取

  • 步驟6:根據(jù)需要設(shè)置一系列的推送設(shè)置,如用戶別名、標(biāo)簽等等

接下來,我們來按照上面的步驟,一步步來實現(xiàn)一個簡易的小米推送Demo

3. 實例解析

步驟1:在小米推送平臺進(jìn)行相關(guān)注冊開發(fā)者賬號,并進(jìn)行應(yīng)用的注冊:應(yīng)用包名,AppID和AppKey

注意,填入的包名要跟你的應(yīng)用App的包名是一致的

創(chuàng)建應(yīng)用
AppID和Key

步驟2:將小米推送的SDK包加入到你應(yīng)用的庫里

放入到app/libs文件夾下,然后右鍵點擊add as Library,最后點擊Model就導(dǎo)入成功了
點擊此處進(jìn)行下載

小米推送SDK
導(dǎo)入包

步驟3:在應(yīng)用內(nèi)初始化小米推送服務(wù)

為了提高推送服務(wù)的注冊率,我選擇在Application的onCreate中初始化推送服務(wù)

BaseActivity.java

package scut.carson_ho.demo_mipush;

import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.os.Process;

import com.xiaomi.mipush.sdk.MiPushClient;

import java.util.List;

/**
 * Created by Carson_Ho on 16/10/26.
 */

    //主要要繼承Application
public class BaseActivity extends Application {
    // 使用自己APP的ID(官網(wǎng)注冊的)
    private static final String APP_ID = "2882303761517520369";
    // 使用自己APP的Key(官網(wǎng)注冊的)
    private static final String APP_KEY = "5401752085369";


    //為了提高推送服務(wù)的注冊率,我建議在Application的onCreate中初始化推送服務(wù)
    //你也可以根據(jù)需要,在其他地方初始化推送服務(wù)
    @Override
    public void onCreate() {
        super.onCreate();


        if (shouldInit()) {
            //注冊推送服務(wù)
            //注冊成功后會向DemoMessageReceiver發(fā)送廣播
            // 可以從DemoMessageReceiver的onCommandResult方法中MiPushCommandMessage對象參數(shù)中獲取注冊信息
            MiPushClient.registerPush(this, APP_ID, APP_KEY);
        }
    }

    //通過判斷手機(jī)里的所有進(jìn)程是否有這個App的進(jìn)程
    //從而判斷該App是否有打開
    private boolean shouldInit() {

    //通過ActivityManager我們可以獲得系統(tǒng)里正在運行的activities
    //包括進(jìn)程(Process)等、應(yīng)用程序/包、服務(wù)(Service)、任務(wù)(Task)信息。
        ActivityManager am = ((ActivityManager) getSystemService(Context.ACTIVITY_SERVICE));
        List<ActivityManager.RunningAppProcessInfo> processInfos = am.getRunningAppProcesses();
        String mainProcessName = getPackageName();

        //獲取本App的唯一標(biāo)識
        int myPid = Process.myPid();
        //利用一個增強(qiáng)for循環(huán)取出手機(jī)里的所有進(jìn)程
        for (ActivityManager.RunningAppProcessInfo info : processInfos) {
            //通過比較進(jìn)程的唯一標(biāo)識和包名判斷進(jìn)程里是否存在該App
            if (info.pid == myPid && mainProcessName.equals(info.processName)) {
                return true;
            }
        }
        return false;
    }
}

注意要在Android.manifest.xml里的application里加入

android:name=".BaseActivity"

這樣在應(yīng)用初始化時是第一個加載BaseActivity.java類文件的
如下圖:

示意圖

步驟4:設(shè)置子類繼承PushMessageReceiver,并復(fù)寫相關(guān)推送消息的方法

Mipush_Broadcast.java

package scut.carson_ho.demo_mipush;

import android.content.Context;

import com.xiaomi.mipush.sdk.ErrorCode;
import com.xiaomi.mipush.sdk.MiPushClient;
import com.xiaomi.mipush.sdk.MiPushCommandMessage;
import com.xiaomi.mipush.sdk.MiPushMessage;
import com.xiaomi.mipush.sdk.PushMessageReceiver;

/**
 * Created by Carson_Ho on 16/10/26.
 */

public class Mipush_Broadcast extends PushMessageReceiver {

    //透傳消息到達(dá)客戶端時調(diào)用
    //作用:可通過參數(shù)message從而獲得透傳消息,具體請看官方SDK文檔
    @Override
    public void onReceivePassThroughMessage(Context context, MiPushMessage message) {

        //打印消息方便測試
        System.out.println("透傳消息到達(dá)了");
        System.out.println("透傳消息是"+message.toString());

    }


//通知消息到達(dá)客戶端時調(diào)用
    //注:應(yīng)用在前臺時不彈出通知的通知消息到達(dá)客戶端時也會回調(diào)函數(shù)
    //作用:通過參數(shù)message從而獲得通知消息,具體請看官方SDK文檔

    @Override
    public void onNotificationMessageArrived(Context context, MiPushMessage message) {
        //打印消息方便測試
        System.out.println("通知消息到達(dá)了");
        System.out.println("通知消息是"+message.toString());
    }

    //用戶手動點擊通知欄消息時調(diào)用
    //注:應(yīng)用在前臺時不彈出通知的通知消息到達(dá)客戶端時也會回調(diào)函數(shù)
    //作用:1. 通過參數(shù)message從而獲得通知消息,具體請看官方SDK文檔
    //2. 設(shè)置用戶點擊消息后打開應(yīng)用 or 網(wǎng)頁 or 其他頁面

    @Override
    public void onNotificationMessageClicked(Context context, MiPushMessage message) {

        //打印消息方便測試
        System.out.println("用戶點擊了通知消息");
        System.out.println("通知消息是" + message.toString());
        System.out.println("點擊后,會進(jìn)入應(yīng)用" );

    }

    //用來接收客戶端向服務(wù)器發(fā)送命令后的響應(yīng)結(jié)果。
    @Override
    public void onCommandResult(Context context, MiPushCommandMessage message) {

        String command = message.getCommand();
        System.out.println(command );
        

        if (MiPushClient.COMMAND_REGISTER.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                
                //打印信息便于測試注冊成功與否
                System.out.println("注冊成功");

            } else {
                System.out.println("注冊失敗");
            }
        }
    }

    //用于接收客戶端向服務(wù)器發(fā)送注冊命令后的響應(yīng)結(jié)果。
    @Override
    public void onReceiveRegisterResult(Context context, MiPushCommandMessage message) {

        String command = message.getCommand();
        System.out.println(command );
    
        if (MiPushClient.COMMAND_REGISTER.equals(command)) {
            if (message.getResultCode() == ErrorCode.SUCCESS) {
                
                //打印日志:注冊成功
                System.out.println("注冊成功");
            } else {
                //打印日志:注冊失敗
                System.out.println("注冊失敗");
            }
        } else {
            System.out.println("其他情況"+message.getReason());
        }
    }

}

具體設(shè)置請看官方SDK文檔,這里只給出最簡單Demo,不作過多描述

步驟5:在AndroidManifest文件里面配置好權(quán)限、注冊Service和BroadcastReceiver

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="scut.carson_ho.demo_mipush">

    //相關(guān)權(quán)限
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.VIBRATE" />


    //注意這里.permission.MIPUSH_RECEIVE是自身app的包名
    <permission android:name="scut.carson_ho.demo_mipush.permission.MIPUSH_RECEIVE" android:protectionLevel="signature" />

    //注意這里.permission.MIPUSH_RECEIVE是自身app的包名
    <uses-permission android:name="scut.carson_ho.demo_mipush.permission.MIPUSH_RECEIVE" />

//注意要初始化BaseActivity.java類
    <application
        android:name=".BaseActivity"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>



    //注冊廣播BroadcastReceiver和Service
    //都是靜態(tài)注冊,因為要長期處在后臺運行
    //注:共是3個廣播接收器和4個服務(wù),其中包括繼承了PushMessageReceiver的DemoMessageReceiver

    //4個后臺服務(wù)
    <service
        android:enabled="true"
        android:process=":pushservice"
        android:name="com.xiaomi.push.service.XMPushService"/>

    //此service必須在3.0.1版本以后(包括3.0.1版本)加入
    <service
        android:name="com.xiaomi.push.service.XMJobService"
        android:enabled="true"
        android:exported="false"
        android:permission="android.permission.BIND_JOB_SERVICE"
        android:process=":pushservice" />

    //此service必須在2.2.5版本以后(包括2.2.5版本)加入
    <service
        android:enabled="true"
        android:exported="true"
        android:name="com.xiaomi.mipush.sdk.PushMessageHandler" />

    <service android:enabled="true"
        android:name="com.xiaomi.mipush.sdk.MessageHandleService" />


    //3個廣播
    <receiver
        android:exported="true"
        android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver" >
        <intent-filter>
            <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </receiver>

    <receiver
        android:exported="false"
        android:process=":pushservice"
        android:name="com.xiaomi.push.service.receivers.PingReceiver" >
        <intent-filter>
            <action android:name="com.xiaomi.push.PING_TIMER" />
        </intent-filter>
    </receiver>

    //繼承了PushMessageReceiver的DemoMessageReceiver的廣播注冊
    <receiver
        android:name=".Mipush_Broadcast"
        android:exported="true">
        <intent-filter>
            <action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" />
        </intent-filter>
        <intent-filter>
            <action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED" />
        </intent-filter>
        <intent-filter>
            <action android:name="com.xiaomi.mipush.ERROR" />
        </intent-filter>
    </receiver>


    </application>
</manifest>

步驟6:根據(jù)需要設(shè)置一系列的推送設(shè)置,如用戶別名、標(biāo)簽等等

  • 此處是簡單Demo,所以不作過多的設(shè)置
  • 更多設(shè)置請回看上方官方Demo解析

運行結(jié)果

測試成功結(jié)果

好了,客戶端的代碼寫好后,可以去小米官網(wǎng)測試一下消息推送了

步驟1:在小米官網(wǎng)的消息推送里選擇你創(chuàng)建的應(yīng)用,然后點擊“推送工具”

點擊推送工具

步驟2:設(shè)置推送消息的相關(guān)信息

可進(jìn)行的配置非常全面,基本上能滿足推送的需求

設(shè)置推送消息

設(shè)置推送消息

推送的結(jié)果

消息到達(dá)客戶端
測試結(jié)果

測試結(jié)果:收到的信息
點擊通知欄消息后

4. Demo下載地址

Carson的Github:Demo_MiPush

5. 關(guān)于對小米推送的思考(問題)

上述說的小米推送看似簡單:初始化推送服務(wù) + 相關(guān)推送設(shè)置。但是,好的代碼不僅能在正常情況下工作,還應(yīng)該充分考慮失敗情況。那么,有什么樣的失敗情況需要我們考慮呢?

  • 背景:在這個初始化推送服務(wù)的過程中,是需要聯(lián)系小米推送的服務(wù)器來申請reg id(即推送token)。
  • 沖突:初始化過程可能失敗:網(wǎng)絡(luò)問題(沒網(wǎng)or網(wǎng)絡(luò)信號弱)、服務(wù)器問題導(dǎo)致初始化失敗。那么,當(dāng)失敗以后,該什么時候再次進(jìn)行初始化呢?

小米推送的Demo里并沒有相關(guān)措施解決這個問題

  • 解決方案:在初始化失敗的情況下提供重試機(jī)制,直到初始化成功(可以通過檢測是否已經(jīng)拿到推送token來確定),問題解決的邏輯如下:
解決邏輯
  • 具體代碼在這里就不作過多描述,如果你希望獲得含注冊重試機(jī)制的小米推送源代碼,請在評論留下你的郵箱,我將親自發(fā)送到你的郵箱
  1. 知識點涵蓋:網(wǎng)絡(luò)數(shù)據(jù)的檢測 & 廣播接收器
  2. 具體請看我寫的另外兩篇文章:
    Android:BroadcastReceiver廣播接收器最全面解析
    Android:檢測網(wǎng)絡(luò)狀態(tài)&監(jiān)聽網(wǎng)絡(luò)變化

總結(jié)

全面考慮到所有異常問題并恰當(dāng)?shù)剡M(jìn)行處理才能真正體現(xiàn)程序猿的功力,希望大家做擼代碼的時候不要只做代碼的搬運工,純粹寫代碼并不會讓你成長,關(guān)鍵在于思考。

6. 總結(jié)


請點贊!因為你的鼓勵是我寫作的最大動力!

相關(guān)文章閱讀
Android開發(fā):最全面、最易懂的Android屏幕適配解決方案
Android開發(fā):Handler異步通信機(jī)制全面解析(包含Looper、Message Queue)
Android開發(fā):頂部Tab導(dǎo)航欄實現(xiàn)(TabLayout+ViewPager+Fragment)
Android開發(fā):底部Tab菜單欄實現(xiàn)(FragmentTabHost+ViewPager)
Android開發(fā):JSON簡介及最全面解析方法!
Android開發(fā):XML簡介及DOM、SAX、PULL解析對比


歡迎關(guān)注Carson_Ho的簡書!

不定期分享關(guān)于安卓開發(fā)的干貨,追求短、平、快,但卻不缺深度

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,936評論 25 709
  • 1.背景問題:成功銷售人員事先做好準(zhǔn)備工作,從其他來源找到與事實有關(guān)的基本信息,不提問不必要的背景問題,很少提問背...
    努力就會看到希望閱讀 345評論 0 0
  • 今天第一天回學(xué)校上課,心里百般滋味。早上到校沒有第一時間去自己的班級,因為不敢去。沒回來之前,現(xiàn)任班主任說孩子們一...
    十三夕閱讀 361評論 3 1
  • 何不做一盞路燈 與清月爭寵 只待你路過 許給你夜行的方向 墨色的石板上 你影只行單 循風(fēng)而來的 是你的微香 你來時...
    難為水_閱讀 568評論 0 0
  • 【原創(chuàng)】銘記時光之十五 前幾日因工作需要把大學(xué)的教材書翻了出來,不料還收獲了一個小驚喜,在書中發(fā)現(xiàn)了一張...
    木本秋閱讀 2,615評論 0 0

友情鏈接更多精彩內(nèi)容