一次 NotificationListenerService 体验

上个礼拜给别的公司团队做一个自己用的监听通知的app,需求是这样的,收款方展示支付宝二维码,当付款人扫码付款成功之后,收款方在app能看到拦截下来的支付宝信息(收款金额,付款人,语音播报的内容等等),收到拦截消息之后及时刷新页面并把金额提交给后台(不可重复提交,提交失败也记录本地),并发出提示音,数据需保存本地,还有商户区分,app可以增加或修改不同的商户,每个商户对应各自的数据(今日统计,收款列表),存在无商户的情况下也必须拦截通知保存本地,也就是不可忽略通知消息。前提该拦截的通知只要支付宝的,过滤其他,如微信,QQ,短信等等。

缕了思路之后便开始动手了。坑爹的需求没有UI,没有效果图,全靠自己脑袋。

最后的效果图就这样:
首页

商户

设置

当前商户包括商户名、商户对应的域名、密钥。首页的“启动”,“停止”是针对该商户是否开启把拦截的金额发送给后台,“测试”是校验该域名地址是否可用,“更换”是切换不同的商户(对应的域名、密钥都是独立的),订单比较多,首页需有分页功能,筛选成功和失败的记录,今日统计也是统计单个商户的。可以存在手动刷新,默认是自动刷新列表及统计数据。

商户也配置商户信息,增加,修改,里面业务增加需判断是否存在,如果当前商户正在使用(服务开启中),不能修改。如果首页选中了“错误1”的商户(服务停止状态),此时可以修改,修改成功后,首页必须刷新修改后的商户。

设置页,声音提醒开启或关闭,清除两天前记录的功能,测试通知的用意是手机拦截是否会通过NotificationListenerService 的 onNotificationPosted方法,拦截日志是指app拦截支付宝的所有通知都要保存下来,方便查看,崩溃日志是为了测试用的,程序崩了之后记录异常日志(空指针,运行异常等等),然后1秒之后重启app继续监听。

之所以增加这些测试、拦截 崩溃日志,是因为运营团队比较死板,只用小米手机,小米手机又是android开发的祸害,进程限制,系统服务强制性的莫名杀死等原因,又是远程对接,自己用的测试机三星,华为,很简单很好实现的一个app,过程却是很痛苦,很让人头疼。

以下说编码过程:
1.本地数据库用郭神的litepal 2.0
2.数据请求用的是retrofit 2.3.0
3.初始化控件使用 butterknife 8.5.1

打开app的gradle导入对应的jar

    compile 'com.jakewharton:butterknife:8.5.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'

    // Retrofit库
    compile 'com.squareup.retrofit2:retrofit:2.3.0'
    compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    // gson解析,可以自行替换
    compile 'com.squareup.retrofit2:converter-gson:2.3.0'
    compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
    // 日志的拦截器,也可以自行选择
    compile 'com.squareup.okhttp3:logging-interceptor:3.6.0'
    compile 'io.reactivex.rxjava2:rxjava:2.0.1'
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'

    compile 'com.kaopiz:kprogresshud:1.0.5'

    compile 'org.litepal.android:core:2.0.0'

MainActivity代码如下:

package com.allen.mynotification;

import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.StrictMode;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewPager;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.widget.RadioButton;
import android.widget.RadioGroup;

import com.allen.mynotification.adapter.ViewPagerAdapter;
import com.allen.mynotification.base.AppBaseActivity;
import com.allen.mynotification.bean.PayRecord;
import com.allen.mynotification.db.PayRecordDb;
import com.allen.mynotification.dialog.ManagerDialog;
import com.allen.mynotification.fragment.HomeFragment;
import com.allen.mynotification.fragment.OtherFragment;
import com.allen.mynotification.fragment.SettingFragment;
import com.allen.mynotification.permission.PermissionsActivity;
import com.allen.mynotification.permission.PermissionsChecker;
import com.allen.mynotification.util.ApiAdress;
import com.allen.mynotification.util.AppUtils;
import com.allen.mynotification.util.LogUtil;
import com.allen.mynotification.util.MD5Utils;
import com.allen.mynotification.util.NetworkUtil;
import com.allen.mynotification.util.RetrofitUtil;
import com.allen.mynotification.util.SharePreferencesUtils;
import com.allen.mynotification.util.SoundHelper;
import com.allen.mynotification.util.StyleToastUtil;
import com.allen.mynotification.util.UrlValidateUtils;
import com.allen.mynotification.util.VibrationHelper;
import com.allen.mynotification.view.NoScrollViewPager;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.ArrayList;

import butterknife.BindView;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import okhttp3.ResponseBody;

/**
 * @author: Allen
 * @date: 2018/9/6
 * @description: Main
 */
public class MainActivity extends AppBaseActivity implements RadioGroup.OnCheckedChangeListener {
    private ArrayList<Fragment> fragments = new ArrayList<>();
    private ArrayList<RadioButton> buttons = new ArrayList<>();
    @BindView(R.id.viewpager)
    public NoScrollViewPager viewPager;
    @BindView(R.id.all_menu)
    RadioGroup all_menu;
    @BindView(R.id.rb_home)
    RadioButton rb_home;
    @BindView(R.id.rb_other)
    RadioButton rb_other;
    @BindView(R.id.rb_setting)
    RadioButton rb_setting;

    public static String useId="0";//正在使用的域名
    //广播
    private MessageRecever recever;
    public HomeFragment homeFragment;
    private long exitTime = 0; // 用来计算返回键的点击间隔时间
    private int minCount = 0;//记录分钟
    private int recLen;//秒数
    private Handler handler = new Handler();
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if (recLen > 0) {
                recLen--;
                handler.postDelayed(this, 1000);
            } else {
                String serviceState = SharePreferencesUtils.getString(mContext, "service", "close");
                if ("open".equals(serviceState)) {
                    if (!NetworkUtil.isConnected(MainActivity.this)) {
                        SoundHelper.getDanger(); //声音
                        VibrationHelper.genInstance(MainActivity.this).openVibration(); //震动
                    }
                    if (!TextUtils.isEmpty(homeFragment.publicHost)) {
                        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().build());
                        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build());
                        httpCode = UrlValidateUtils.isConnect(homeFragment.publicHost);
                        if (200 == httpCode) { //如果与服务器连接成功,刷新时间
                            homeFragment.tv_date.setText(String.format(getResources().getString(R.string.last_socket_time), AppUtils.getCurrent()));
                            minCount = 0;
                        } else {
                            minCount++;
                            if (minCount % 3 == 0) { //三分钟报警
                                SoundHelper.getDanger();
                            }
                        }
                    }
                }
                recLen = 60;//初始秒数
                handler.postDelayed(runnable, 1000);
            }
        }
    };
    //权限
    private static final int REQUEST_CODE = 0x1001;//权限请求码
    private PermissionsChecker permissionsChecker;
    //     所需的全部权限
    static final String[] PERMISSIONS = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,//写入权限
            Manifest.permission.READ_EXTERNAL_STORAGE,  //读取权限
    };
    public static int httpCode;

    @Override
    protected int setLayout() {
        return R.layout.activity_main;
    }

    @Override
    protected void initClick() {
        //程序打开,服务默认关闭
        SharePreferencesUtils.setString(mContext, "service", "close");
        //开始计时检查网络
        recLen = 60;//初始秒数
        handler.postDelayed(runnable, 1000);

        permissionsChecker = new PermissionsChecker(this);
        showPermission();//检测权限
        buttons.add(rb_home);
        buttons.add(rb_other);
        buttons.add(rb_setting);
        homeFragment = new HomeFragment();
        fragments.add(homeFragment); //首页
        fragments.add(new OtherFragment()); //商户
        fragments.add(new SettingFragment()); //设置

        all_menu.setOnCheckedChangeListener(this);
        viewPager.setAdapter(new ViewPagerAdapter(getSupportFragmentManager(), fragments));
        viewPager.setCurrentItem(0, false);//初始选中第一个fragment
        viewPager.setOffscreenPageLimit(fragments.size() - 1);
        viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }

            @Override
            public void onPageSelected(int position) {
                switch (position) {
                    case 0://主页
                        changeBottom(rb_home);
                        break;
                    case 1://商户
                        changeBottom(rb_other);
                        break;
                    case 2://设置
                        changeBottom(rb_setting);
                        break;
                }
            }

            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });
    }

    @Override
    protected void initData() {
        //没有授权
        if (!AppUtils.isNotificationManagerEnabled(getApplicationContext())) {
            ManagerDialog dialog = new ManagerDialog(MainActivity.this);
            dialog.show();
        }
        //注册广播
        registerMessageReceiver();
    }

    @Override
    public void onCheckedChanged(RadioGroup radioGroup, int i) {
        switch (i) {
            case R.id.rb_home://首页
                viewPager.setCurrentItem(0, false);
                break;
            case R.id.rb_other://商户
                viewPager.setCurrentItem(1, false);
                break;
            case R.id.rb_setting://设置
                viewPager.setCurrentItem(2, false);
                break;
        }
    }

    //注册广播
    private void registerMessageReceiver() {
        recever = new MessageRecever();
        IntentFilter filter = new IntentFilter();
        filter.addAction(AppUtils.JPUSH_DATA);
        registerReceiver(recever, filter);
    }

    //改变按钮状态
    private void changeBottom(RadioButton button) {
        for (int i = 0; i < buttons.size(); i++) {
            if (button == buttons.get(i)) {
                buttons.get(i).setChecked(true);
            } else {
                buttons.get(i).setChecked(false);
            }
        }
    }

    /**
     * 广播接收
     */
    public class MessageRecever extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (AppUtils.JPUSH_DATA.equals(intent.getAction())) {
//                //重新去刷新数据
                PayRecord payRecord = (PayRecord) intent.getSerializableExtra("payRecord");
                if (payRecord != null) {
                    String firstMD5 = MD5Utils.getMD5(payRecord.getPayMoney() + ApiAdress.alway, false, 32);
                    String secondMD5 = MD5Utils.getMD5(firstMD5 + homeFragment.publicKey, false, 32);
                    //发送请求
                    requestSendMoney(homeFragment.publicHost, payRecord.getPayMoney(), ApiAdress.alway, secondMD5, payRecord);
                }
            }
        }
    }

    /**
     * 请求接口
     *
     * @param price
     * @param type
     * @param sign
     */
    private void requestSendMoney(String url, String price, String type, String sign, final PayRecord payRecord) {
        String serviceState = SharePreferencesUtils.getString(mContext, "service", "close");
        final String sound = SharePreferencesUtils.getString(MyApplication.getContext(), "sound", "open");
        if ("open".equals(serviceState)) {
            if (TextUtils.isEmpty(url)) {
                //设置数据
                PayRecordDb.genInstance().setMsgAndDate(payRecord.getId(), "域名为空", AppUtils.getCurrent(), "失败", useId);
                playSound(sound, 2);
                StyleToastUtil.error("请切换商户");
                return;
            }
            //无效api拒绝
            if (200 != httpCode) {
                //设置数据
                PayRecordDb.genInstance().setMsgAndDate(payRecord.getId(), "无效api", AppUtils.getCurrent(), "失败", useId);
                playSound(sound, 2);
                return;
            }
            RetrofitUtil.getInstance().initRetrofit(url).sendMoney(price, type, sign)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new Observer<ResponseBody>() {
                        @Override
                        public void onSubscribe(Disposable d) {
                        }

                        @Override
                        public void onNext(ResponseBody value) {
                            try {
                                String jsonContent = value.string().trim();
                                if (!TextUtils.isEmpty(jsonContent)) {
                                    JSONObject json = new JSONObject(jsonContent);
                                    String code = json.getString("code");
                                    String msg = json.getString("msg");
                                    if ("1".equals(code)) {
                                        //设置数据
                                        PayRecordDb.genInstance().setMsgAndDate(payRecord.getId(), msg, AppUtils.getCurrent(), "成功", useId);
                                        playSound(sound, 1);
                                    } else {
                                        //设置数据
                                        PayRecordDb.genInstance().setMsgAndDate(payRecord.getId(), msg, AppUtils.getCurrent(), "失败", useId);
                                        playSound(sound, 2);
                                    }
                                    StyleToastUtil.success(msg);
                                    //最后与服务器的通讯时间
                                    homeFragment.tv_date.setText(String.format(getResources().getString(R.string.last_socket_time), payRecord.getPayDate()));
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            } catch (JSONException e) {
                                e.printStackTrace();
                            }
                        }

                        @Override
                        public void onError(Throwable e) {
                            LogUtil.d("e------->" + e.getMessage());
                            PayRecordDb.genInstance().setMsgAndDate(payRecord.getId(), NetworkUtil.exceptionDispose(e), AppUtils.getCurrent(), "失败", useId);
                            playSound(sound, 2);
                            StyleToastUtil.success(NetworkUtil.exceptionDispose(e));
                        }

                        @Override
                        public void onComplete() {
                        }
                    });
        } else {
            //设置数据
            PayRecordDb.genInstance().setMsgAndDate(payRecord.getId(), "未启动服务", AppUtils.getCurrent(), "失败", useId);
            playSound(sound, 2);
        }
    }

    //    播放声音
    private void playSound(final String sound, final int type) {
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if ("open".equals(sound)) { //播放声音
                    SoundHelper.getSound(type);
                }
            }
        }, 5000);

        //1秒后刷新状态
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                homeFragment.reloadData();//重新加载数据
            }
        }, 2000);
    }

    //去刷新首页的商户
    public void refreshHomeBus(String changeId) {
        if (!TextUtils.isEmpty(useId) && useId.equals(changeId)) { //如果是当前正在用的商户 则刷新
            homeFragment.changeRefreshData();
        }
    }

    /**
     * 检测权限
     */
    private void showPermission() {
        // 缺少权限时, 进入权限配置页面
        if (permissionsChecker.lacksPermissions(PERMISSIONS)) {
            PermissionsActivity.startActivityForResult(this, REQUEST_CODE, PERMISSIONS);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // 拒绝时, 关闭页面, 缺少主要权限, 无法运行
        if (requestCode == REQUEST_CODE && resultCode == PermissionsActivity.PERMISSIONS_DENIED) {
            finish();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //销毁时把服务开关关闭
        handler.removeCallbacks(runnable);
        SharePreferencesUtils.setString(mContext, "service", "close");
        if (recever != null) {
            this.unregisterReceiver(recever);
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK
                && event.getAction() == KeyEvent.ACTION_DOWN) {
            if ((System.currentTimeMillis() - exitTime) > 2000) {
                //弹出提示,可以有多种方式
                StyleToastUtil.warning("再按一次退出程序");
                exitTime = System.currentTimeMillis();
            } else {
                finish();
                System.exit(0);
            }
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }
}

由于要用到本地数据库,所以对6.0以上的系统申请了权限,主体是拦截通知,所以必须要申请系统的通知服务操作,如未打开,需提示用户手动授权,通知采取广播通讯,所以MainActivity也涉及到了广播的注册,接收。定时器任务:1.服务开启中,如果用户中途手机断网,一分钟后需发出报警声及震动,每分钟刷新服务器的通讯时间,2.如果服务关闭存在断网,则三分钟提醒一次。
接收到广播通知后,经过密钥的二次加密,在请求服务端。
接口请求,1.服务开启中,请求服务器数据,前提需要判断url地址不为空,并且url是有效可用的,否则return 发出失败声音,满足条件之后,code!=1时,一律作失败处理,并发出声音和弹出提示。5秒后发出声音,2秒后自动刷新首页列表数据。

项目不大,并没有用什么MVP,MVVM设计模式,普通的MVC足以一目了然,下面看HomeFragment:

package com.allen.mynotification.fragment;

import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.os.StrictMode;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.TextView;

import com.allen.mynotification.MainActivity;
import com.allen.mynotification.R;
import com.allen.mynotification.activity.PayRecordDeatail;
import com.allen.mynotification.adapter.HomeAdapter;
import com.allen.mynotification.base.AppBaseFragment;
import com.allen.mynotification.bean.Business;
import com.allen.mynotification.bean.PayRecord;
import com.allen.mynotification.db.BusinessDb;
import com.allen.mynotification.db.PayRecordDb;
import com.allen.mynotification.util.AppUtils;
import com.allen.mynotification.util.DensityUtil;
import com.allen.mynotification.util.LogUtil;
import com.allen.mynotification.util.SharePreferencesUtils;
import com.allen.mynotification.util.StyleToastUtil;
import com.allen.mynotification.util.UrlValidateUtils;
import com.allen.mynotification.view.DropDownListPopu;

import java.util.List;
import java.util.Vector;

import butterknife.BindView;
import butterknife.OnClick;

/**
 * @author: Allen
 * @date: 2018/9/6
 * @description: 首页
 */

public class HomeFragment extends AppBaseFragment implements AdapterView.OnItemClickListener {
    public HomeAdapter adapter;
    @BindView(R.id.tv_current_name)
    TextView tv_current_name;
    @BindView(R.id.tv_change)
    TextView tv_change;
    @BindView(R.id.tv_host_api)
    public TextView tv_host_api;
    @BindView(R.id.tv_date)
    public TextView tv_date;
    @BindView(R.id.recyclerview)
    RecyclerView recyclerview;
    @BindView(R.id.btn_start)
    Button btn_start;
    @BindView(R.id.btn_stop)
    Button btn_stop;

    @BindView(R.id.tv_total_page)
    TextView tv_total_page;
    @BindView(R.id.tv_last_page)
    TextView tv_last_page;
    @BindView(R.id.tv_next_page)
    TextView tv_next_page;
    @BindView(R.id.tv_today_total)
    TextView tv_today_total;
    @BindView(R.id.tv_refresh)
    TextView tv_refresh;

    @BindView(R.id.cb_screen_success)
    CheckBox cb_screen_success;
    @BindView(R.id.cb_screen_fail)
    CheckBox cb_screen_fail;

    @BindView(R.id.tv_test)
    TextView tv_test;
    private int currentPage = 1;
    public int currentSize = 8;
    private int totalPage;
    //公共的
    public String publicKey;
    public String publicHost;

    private DropDownListPopu<Business> ddp;
    private List<Business> list = new Vector<>();
    public List<PayRecord> payList = new Vector<>();
    private MainActivity mainAct;
    private boolean pageIsScreen = false;
    private String screenStr;

    @Override
    protected int getLayoutId() {
        return R.layout.fragment_home;
    }

    @Override
    protected void initClick() {
        GridLayoutManager layoutManager = new GridLayoutManager(mContext, 1);
        recyclerview.setLayoutManager(layoutManager);

        adapter = new HomeAdapter(mContext, payList);
        recyclerview.setAdapter(adapter);


        adapter.setOnLongClickListener(new HomeAdapter.ViewClickListener() {
            @Override
            public void onLongClick(View view, final int position) {
                AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
                builder.setMessage("确定删除吗?");
                builder.setTitle("提示");

                //添加AlertDialog.Builder对象的setPositiveButton()方法
                builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        if (payList != null && payList.size() > 0) {
                            PayRecordDb.genInstance().deleteItemById(payList.get(position).getId(), Integer.parseInt(mainAct.useId)); //删除数据库中的数据
                            LogUtil.d("date-->" + payList.get(position).getPayDate() + "条数:" + position);
                            payList.remove(position);
                            StyleToastUtil.success("删除成功");
                            if (payList != null && payList.size() > 0) {
                                tv_date.setText(String.format(getResources().getString(R.string.last_socket_time), payList.get(0).getPayDate()));
                            } else {
                                tv_date.setText(String.format(getResources().getString(R.string.last_socket_time), "--"));
                            }
                        } else {
                            StyleToastUtil.error("删除失败");
                        }
                        adapter.notifyDataSetChanged();
                    }
                });
                //添加AlertDialog.Builder对象的setNegativeButton()方法
                builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                    }
                });
                builder.create().show();
            }

            @Override
            public void onViewClick(View view, int position) {
                Bundle bundle = new Bundle();
                bundle.putSerializable("payDetail", PayRecordDb.genInstance().getOneData(payList.get(position).getId(), Integer.parseInt(mainAct.useId)));
                Intent intent = new Intent(mContext, PayRecordDeatail.class);
                intent.putExtras(bundle);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void lazyLoadData() {
        String serviceState = SharePreferencesUtils.getString(mContext, "service", "close");
        if ("open".equals(serviceState)) {
            btn_stop.setBackground(getResources().getDrawable(R.drawable.bg_rectangle_line));
            btn_start.setBackground(getResources().getDrawable(R.drawable.bg_silery_rectangle_line));
        } else if ("close".equals(serviceState)) {
            btn_start.setBackground(getResources().getDrawable(R.drawable.bg_rectangle_line));
            btn_stop.setBackground(getResources().getDrawable(R.drawable.bg_silery_rectangle_line));
        }

        tv_current_name.setText(String.format(getResources().getString(R.string.current_user), "--"));
        mainAct = (MainActivity) getActivity();

        reloadData();
    }

    //加载数据
    public void reloadData() {
        refreshPage();
        tv_today_total.setText(String.format(mContext.getResources().getString(R.string.today_total), PayRecordDb.genInstance().getToDayTotal(AppUtils.getToDay(), Integer.parseInt(mainAct.useId)) + "", PayRecordDb.genInstance().getSuccessTotal(AppUtils.getToDay(), Integer.parseInt(mainAct.useId)) + "", PayRecordDb.genInstance().getFailTotal(AppUtils.getToDay(), Integer.parseInt(mainAct.useId)) + ""));
    }

    /**
     * 点击事件
     */
    @OnClick({R.id.tv_change, R.id.btn_start, R.id.btn_stop, R.id.tv_last_page, R.id.tv_next_page, R.id.tv_test, R.id.cb_screen_success, R.id.cb_screen_fail, R.id.tv_refresh})
    public void clickBtn(View view) {
        switch (view.getId()) {
            case R.id.tv_change: //更换
                changeApi();
                break;
            case R.id.tv_test: //测试
                if (!TextUtils.isEmpty(publicHost)) {
                    checkUrl();
                } else {
                    StyleToastUtil.warning("请先切换对应的域名!");
                }
                break;
            case R.id.btn_start: //启动
                if (TextUtils.isEmpty(publicHost)) {
                    StyleToastUtil.success("商户不能为空!");
                    return;
                }
                SharePreferencesUtils.setString(mContext, "service", "open");
                StyleToastUtil.success("服务启动");
                btn_start.setBackground(getResources().getDrawable(R.drawable.bg_silery_rectangle_line));
                btn_stop.setBackground(getResources().getDrawable(R.drawable.bg_rectangle_line));
                btn_start.setEnabled(false);
                btn_stop.setEnabled(true);
                checkUrl();
                break;
            case R.id.btn_stop: //停止
                SharePreferencesUtils.setString(mContext, "service", "close");
                StyleToastUtil.success("服务停止");
                btn_stop.setBackground(getResources().getDrawable(R.drawable.bg_silery_rectangle_line));
                btn_start.setBackground(getResources().getDrawable(R.drawable.bg_rectangle_line));
                btn_start.setEnabled(true);
                btn_stop.setEnabled(false);
                break;
            case R.id.tv_last_page:// 上一页
                if (currentPage > 1) {
                    currentPage--;
                    currentSize -= 8;
                    pageOpration();
                }
                break;
            case R.id.tv_next_page://下一页
                if (currentPage < totalPage) {
                    currentPage++;
                    currentSize += 8;
                    pageOpration();
                }
                break;
            case R.id.cb_screen_success:
                if (cb_screen_success.isChecked()) {
                    refreshScreen("成功");
                    cb_screen_fail.setChecked(false);
                } else {
                    refreshPage();
                }
                break;
            case R.id.cb_screen_fail:
                if (cb_screen_fail.isChecked()) {
                    refreshScreen("失败");
                    cb_screen_success.setChecked(false);
                } else {
                    refreshPage();
                }
                break;
            case R.id.tv_refresh:
                reloadData();
                break;
        }
    }
    //上下页操作数据
    private void pageOpration(){
        payList = PayRecordDb.genInstance().getPageTotal(currentPage, currentSize, pageIsScreen, screenStr, Integer.parseInt(mainAct.useId));
        adapter.setData(payList);
        tv_total_page.setText(currentPage + "/" + totalPage);
    }

    //刷新筛选
    public void refreshScreen(String state) {
        pageIsScreen = true;
        screenStr = state;
        currentPage = 1;
        currentSize = 8;
        payList = PayRecordDb.genInstance().query(currentSize, true, state, Integer.parseInt(mainAct.useId)); //查找数据库中的数据
        adapter.setData(payList);

        //记录总页数
        int totalSu = PayRecordDb.genInstance().getTotalPage(true, state, Integer.parseInt(mainAct.useId));
        totalPage = totalSu / currentSize + (totalSu % currentSize > 0 ? 1 : 0);
        tv_total_page.setText(currentPage + "/" + totalPage);
    }

    private void checkUrl() {
        if (!TextUtils.isEmpty(publicHost)) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().build());
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build());
            int code = UrlValidateUtils.isConnect(publicHost);
            if (200 == code) { //如果与服务器连接成功,刷新时间
                mainAct.httpCode = code;
                tv_date.setText(String.format(getResources().getString(R.string.last_socket_time), AppUtils.getCurrent()));
                StyleToastUtil.success("连接成功!");
            } else {
                mainAct.httpCode = 0;
                StyleToastUtil.error("连接失败,请检查域名!");
            }
        }
    }

    //刷新分页
    private void refreshPage() {
        pageIsScreen = false;
        screenStr = "";
        //页数条数重新初始
        currentPage = 1;
        currentSize = 8;
        payList = PayRecordDb.genInstance().query(currentSize, false, null, Integer.parseInt(mainAct.useId)); //查找数据库中的数据
        adapter.setData(payList);
        //记录总页数
        int total = PayRecordDb.genInstance().getTotalPage(false, null, Integer.parseInt(mainAct.useId));
        totalPage = total / currentSize + (total % currentSize > 0 ? 1 : 0);
        tv_total_page.setText(currentPage + "/" + totalPage);
    }


    //更换域名
    private void changeApi() {
        String service = SharePreferencesUtils.getString(mContext, "service", "open");
        if ("open".equals(service)) {
            StyleToastUtil.warning("请先关闭服务!");
            return;
        }
        list = BusinessDb.genInstance().queryAll();
        //item首行添加无商户
        Business business = new Business("无商户", "", "", AppUtils.getCurrent());
        list.add(0, business);

        if (list != null && list.size() > 0) {
            if (ddp == null) {
                ddp = new DropDownListPopu(getActivity(), list, this);
                ddp.setWidth(DensityUtil.getDevicePx(getActivity())[0] - DensityUtil.dip2px(getActivity(), 10));
            } else {
                ddp.setData(list);
            }
            ddp.showAsDropDown(findViewById(R.id.tv_change), 0, DensityUtil.dip2px(getActivity(), 10));
        } else {
            StyleToastUtil.error("还未添加商户");
        }
    }

    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
        if (list != null && list.size() > 0) {
            String str = list.get(i).getBusHost();
            tv_host_api.setText(str);
            publicHost = str;
            publicKey = list.get(i).getBusKey();
            tv_current_name.setText(String.format(mContext.getResources().getString(R.string.current_user), list.get(i).getBusName()));

            if (mainAct != null) {
                mainAct.useId = list.get(i).getId() + "";
            }
            reloadData();//切换商户刷新数据
            ddp.dismiss();
        }
    }

    //修改之后刷新首页的数据
    public void changeRefreshData() {
        List<Business> list = BusinessDb.genInstance().queryExisForHost(Integer.parseInt(mainAct.useId));
        if (list != null && list.size() > 0) {
            publicHost = list.get(0).getBusHost();
            tv_host_api.setText(publicHost);
            publicKey = list.get(0).getBusKey();
            tv_current_name.setText(String.format(mContext.getResources().getString(R.string.current_user), list.get(0).getBusName()));
        }
    }
}

所有的页面操作业务都在这了,public List<PayRecord> payList = new Vector<>(); 是预防多并发时的线程安全,通常用ArrayList,列表支持长按删除,点击跳转详情页,上下分页操作,都有注释的。

OtherFragment 以下所示:

package com.allen.mynotification.fragment;

import android.content.DialogInterface;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;

import com.allen.mynotification.MainActivity;
import com.allen.mynotification.R;
import com.allen.mynotification.adapter.OtherAdapter;
import com.allen.mynotification.base.AppBaseFragment;
import com.allen.mynotification.bean.Business;
import com.allen.mynotification.db.BusinessDb;
import com.allen.mynotification.util.AppUtils;
import com.allen.mynotification.util.LogUtil;
import com.allen.mynotification.util.SharePreferencesUtils;
import com.allen.mynotification.util.StyleToastUtil;
import com.allen.mynotification.view.ClearEditText;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.OnClick;

/**
 * @author: Allen
 * @date: 2018/9/6
 * @description: 其他
 */

public class OtherFragment extends AppBaseFragment {
    @BindView(R.id.et_business_name)
    ClearEditText et_business_name;
    @BindView(R.id.et_host_name)
    ClearEditText et_host_name;
    @BindView(R.id.et_key)
    ClearEditText et_key;
    @BindView(R.id.recyclerview)
    RecyclerView recyclerview;
    @BindView(R.id.tv_add)
    TextView tv_add;
    @BindView(R.id.tv_change)
    TextView tv_change;
    boolean isSave = false;

    private OtherAdapter adapter;
    private List<Business> listAll = new ArrayList<>();
    private String currentDate;
    private int changId;
    private MainActivity mainAct;

    @Override
    protected int getLayoutId() {
        return R.layout.fragment_other;
    }


    @Override
    protected void initClick() {
        GridLayoutManager layoutManager = new GridLayoutManager(mContext, 1);
        recyclerview.setLayoutManager(layoutManager);

        listAll = BusinessDb.genInstance().queryAll();
        adapter = new OtherAdapter(mContext, listAll);
        recyclerview.setAdapter(adapter);
        adapter.setOnLongClickListener(new OtherAdapter.ViewClickListener() {

            @Override
            public void onLongClick(View view, final int position) {
                AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
                builder.setMessage("确定删除?");
                builder.setTitle("提示");

                //添加AlertDialog.Builder对象的setPositiveButton()方法
                builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        if (listAll != null && listAll.size() > 0) {
                            BusinessDb.genInstance().deleteItemById(listAll.get(position).getId()); //删除数据库中的数据
                            LogUtil.d("date-->" + listAll.get(position).getBusDate() + "条数:" + position);
                            listAll.remove(position);
                            StyleToastUtil.success("删除成功");
                        } else {
                            StyleToastUtil.error("删除失败");
                        }
                        adapter.notifyDataSetChanged();
                    }
                });
                //添加AlertDialog.Builder对象的setNegativeButton()方法
                builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                    }
                });
                builder.create().show();
            }

            @Override
            public void onViewClick(View view, int position) {
                if (listAll != null && listAll.size() > 0) {
                    Business business = listAll.get(position);
                    String serviceState = SharePreferencesUtils.getString(mContext, "service", "close");
                    if (mainAct != null && !TextUtils.isEmpty(mainAct.useId) && "open".equals(serviceState)) {
                        if (mainAct.useId.equals(business.getId() + "")) {
                            StyleToastUtil.warning("正在启动中,无法修改!");
                            return;
                        }
                    }
                    currentDate = business.getBusDate();
                    et_business_name.setText(business.getBusName());
                    et_host_name.setText(business.getBusHost());
                    et_key.setText(business.getBusKey());
                    changId = business.getId();


                }
            }
        });
    }

    @Override
    protected void lazyLoadData() {
        mainAct = (MainActivity) getActivity();
    }

    @OnClick({R.id.tv_add, R.id.tv_change})
    public void clickBtn(View view) {
        switch (view.getId()) {
            case R.id.tv_add:
                editCheck(1);
                break;
            case R.id.tv_change:
                editCheck(2);
                break;
        }
    }


    //输入框判断
    private void editCheck(int type) {
        String name = et_business_name.getText().toString().trim();
        String apiHost = et_host_name.getText().toString().trim();
        String key = et_key.getText().toString().trim();
        if (TextUtils.isEmpty(name)) {
            StyleToastUtil.warning("商户名不能为空");
        } else if (TextUtils.isEmpty(apiHost)) {
            StyleToastUtil.warning("域名不能为空");
        } else if (TextUtils.isEmpty(key)) {
            StyleToastUtil.warning("密钥不能为空");
        } else {
            String apiHostLast = apiHost.substring(apiHost.length() - 1, apiHost.length());
            if (1 == type) {
                List<Business> list = BusinessDb.genInstance().queryExisForHost(changId);
                if (list == null || list.size() == 0) {
                    currentDate = AppUtils.getCurrent();
                    if (!"/".equals(apiHostLast)) {  //如果末尾没有斜杠则加上
                        apiHost = apiHost + "/";
                    }
                    Business business = new Business(name, apiHost, key, currentDate);
                    business.save();
                    listAll.add(0, business);
                    adapter.setData(listAll);
                    StyleToastUtil.success("增加成功!");

                    et_business_name.setText("");
                    et_host_name.setText("http://");
                    et_key.setText("");
                } else {
                    StyleToastUtil.success("域名已存在!");
                }
            } else if (2 == type) {
                if (!"/".equals(apiHostLast)) {  //如果末尾没有斜杠则加上
                    apiHost = apiHost + "/";
                }
                BusinessDb.genInstance().updateData(name, apiHost, key, currentDate, changId);
                listAll = BusinessDb.genInstance().queryAll();
                adapter.setData(listAll);
                //刷新首页修改的内容
                mainAct.refreshHomeBus(changId + "");
                et_business_name.setText("");
                et_host_name.setText("http://");
                et_key.setText("");
                changId = 0;
                StyleToastUtil.success("修改成功!");
            }
        }
    }
}

设置页面:

package com.allen.mynotification.fragment;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.RemoteViews;

import com.kaopiz.kprogresshud.KProgressHUD;
import com.allen.mynotification.MainActivity;
import com.allen.mynotification.R;
import com.allen.mynotification.activity.ErrorLogActivity;
import com.allen.mynotification.activity.LogActivity;
import com.allen.mynotification.base.AppBaseFragment;
import com.allen.mynotification.db.HistoryRecordDb;
import com.allen.mynotification.db.PayRecordDb;
import com.allen.mynotification.dialog.ProgressHUD;
import com.allen.mynotification.util.SharePreferencesUtils;
import com.allen.mynotification.util.StyleToastUtil;
import com.allen.mynotification.util.VibrationHelper;
import com.allen.mynotification.view.SwitchButton;

import butterknife.BindView;
import butterknife.OnClick;

/**
 * @author: Allen
 * @date: 2018/9/8
 * @description: 设置
 */

public class SettingFragment extends AppBaseFragment implements SwitchButton.OnCheckedChangeListener {
    @BindView(R.id.switch_sound)
    SwitchButton switch_sound;
    @BindView(R.id.layout_clear)
    RelativeLayout layout_clear;
    @BindView(R.id.layout_test_notice)
    RelativeLayout layout_test_notice;
    @BindView(R.id.layout_query_log)
    RelativeLayout layout_query_log;
    @BindView(R.id.layout_error_log)
    RelativeLayout layout_error_log;
    private int countNotify = 0;
    private MainActivity mainAct;

    @Override
    protected int getLayoutId() {
        return R.layout.view_setting;
    }

    @Override
    protected void initClick() {
        switch_sound.setOnCheckedChangeListener(this);
    }

    @Override
    protected void lazyLoadData() {
        mainAct= (MainActivity) getActivity();
        String soundState = SharePreferencesUtils.getString(mContext, "sound", "open");
        if ("open".equals(soundState)) {
            switch_sound.setChecked(true);
        } else if ("close".equals(soundState)) {
            switch_sound.setChecked(false);
        }
    }

    @OnClick({R.id.layout_clear, R.id.layout_test_notice, R.id.layout_query_log, R.id.layout_error_log})
    public void clickBtn(View view) {
        switch (view.getId()) {
            case R.id.layout_clear:
                final KProgressHUD hud = ProgressHUD.show(mContext, "正在删除...");
                new Handler().postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        PayRecordDb.genInstance().deleteData(); //删除前两天的数据
                        HistoryRecordDb.genInstance().deleteData();//日志删除
                        hud.dismiss();
                        StyleToastUtil.success("删除成功!");
                    }
                }, 1000);
                break;
            case R.id.layout_test_notice:
                testNotify();
                break;
            case R.id.layout_query_log:
                Intent intent=new Intent(mContext,LogActivity.class);
                intent.putExtra("busId",mainAct.useId);
                startActivity(intent);
                break;
            case R.id.layout_error_log:
                startActivity(new Intent(mContext, ErrorLogActivity.class));
                break;
        }
    }

    @Override
    public void onCheckedChanged(SwitchButton view, boolean isChecked) {
        switch (view.getId()) {
            case R.id.switch_sound: //声音
                if (isChecked) {
                    SharePreferencesUtils.setString(mContext, "sound", "open");
                } else {
                    SharePreferencesUtils.setString(mContext, "sound", "close");
                }
                break;
        }
    }


    /**
     * 测试是否可以显示通知
     */
    private void testNotify() {
        VibrationHelper.genInstance(mContext).openVibration();
        NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
        Notification.Builder mBuilder = new Notification.Builder(mContext);
        RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.notify);

        mBuilder.setContent(remoteViews)
                .setContentIntent(contentIntent)
                .setTicker("测试支付宝读取通知")
                .setWhen(System.currentTimeMillis())
                .setAutoCancel(true)
                .setOngoing(false)
                .setContentTitle("测试通知")
                .setSmallIcon(mContext.getApplicationInfo().icon)//采用quick fallback image
                .setDefaults(Notification.DEFAULT_ALL);

        Notification notify = mBuilder.build();
        //  notify.flags = Notification.FLAG_NO_CLEAR;//|Notification.FLAG_ONGOING_EVENT;
        notificationManager.notify(countNotify++, notify);
    }
}

留意测试通知RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(), R.layout.notify); 在拦截notification的时候判断是否为项目的包名即可。

接下来重点放在 NotificationListenerService :

package com.allen.mynotification.core;

import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Looper;
import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;

import com.allen.mynotification.MyApplication;
import com.allen.mynotification.bean.HistoryRecord;
import com.allen.mynotification.bean.PayRecord;
import com.allen.mynotification.util.AppUtils;
import com.allen.mynotification.util.LogUtil;
import com.allen.mynotification.util.StyleToastUtil;

import java.util.List;

/**
 * @author: Allen.
 * @date: 2018/9/6
 * @description: 通知栏监听
 */

public class NotificationMonitor extends NotificationListenerService {

    @Override
    public void onCreate() {
        super.onCreate();
        ensureCollectorRunning();
    }

    @Override
    public void onNotificationPosted(final StatusBarNotification sbn) {
        String packageName = sbn.getPackageName();
        final String content = sbn.getNotification().tickerText + "";
        if ("com.eg.android.AlipayGphone".equals(packageName)) {
            if (sbn.getNotification().extras != null) {
                String readContent = sbn.getNotification().extras.getString("android.text");
                if (!TextUtils.isEmpty(content) && !TextUtils.isEmpty(readContent)) {
                    //开始对扫码进行拦截
                    if (content.contains("扫码") || content.contains("收款")) {
                        PayRecord payRecord = new PayRecord(content, AppUtils.getNumber(readContent), AppUtils.longDateToStr(sbn.getNotification().when), "--");
                        payRecord.save();
                        Bundle bundle = new Bundle();
                        bundle.putSerializable("payRecord", payRecord);
                        Intent intent = new Intent();
                        intent.setAction(AppUtils.JPUSH_DATA);
                        //intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);//前台广播(默认是后台广播)
                        intent.putExtras(bundle);
                        sendBroadcast(intent);

                        LogUtil.d("包名:" + packageName);
                        LogUtil.d("具体内容:" + content);
                        LogUtil.d("内容体:" + readContent);
                        LogUtil.d("内容体中的金额为:" + AppUtils.getNumber(readContent));

                        //符合存储信息
                        HistoryRecord history = new HistoryRecord(content, AppUtils.longDateToStr(sbn.getNotification().when), AppUtils.getNumber(readContent), packageName);
                        history.save();
                    }
                }
            }
        }
        if (packageName.equals(AppUtils.getPackageName(MyApplication.getContext()))) {
            if (!TextUtils.isEmpty(content) && content.contains("测试")) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Looper.prepare();
                        StyleToastUtil.success("测试读取通知!");
                        Looper.loop();
                    }
                }).start();
            }
        }


//        //获取PendingItent
//        PendingIntent pendingIntent = sbn.getNotification().contentIntent;
//        Intent intent = AppUtils.getIntentByPendingIntent(pendingIntent);
//        //将Intent转换成String,可以存到数据库
//        String serializeIntent = intent.toUri(0);
//       LogUtil.d("intent-->"+ intent.getData());
//        CustomNotification customNotification = new CustomNotification();
//        customNotification.setSerializeIntent(serializeIntent);
//        //跳转逻辑
//        JumpAppLogic.jumpAppByNotification(customNotification);
//


    }

    @Override
    public void onRebind(Intent intent) {
        try {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
                intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
            } else {
                intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
            }
            startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//            cancelNotification(sbn.getKey());
//        } else {
//            cancelNotification(sbn.getPackageName(), sbn.getTag(), sbn.getId());
//        }
    }


    //确认NotificationMonitor是否开启
    private void ensureCollectorRunning() {
        ComponentName collectorComponent = new ComponentName(this, NotificationMonitor.class);
        ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        boolean collectorRunning = false;
        List<ActivityManager.RunningServiceInfo> runningServices = manager.getRunningServices(Integer.MAX_VALUE);
        if (runningServices == null) {
            return;
        }
        for (ActivityManager.RunningServiceInfo service : runningServices) {
            if (service.service.equals(collectorComponent)) {
                if (service.pid == android.os.Process.myPid()) {
                    collectorRunning = true;
                }
            }
        }
        if (collectorRunning) {
            return;
        }
        toggleNotificationService(MyApplication.getContext());
    }

    /**
     * 重置NotificationService
     */
    public static void toggleNotificationService(Context mContext) {
        PackageManager pm = mContext.getPackageManager();
        pm.setComponentEnabledSetting(new ComponentName(mContext, com.allen.mynotification.core.NotificationMonitor.class),
                PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
        pm.setComponentEnabledSetting(new ComponentName(mContext, com.allen.mynotification.core.NotificationMonitor.class)
                , PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
    }

}

监听过程去匹配支付宝的包名,这边有两个个地方需要注意:1.有些手机获取 tickerText 字段是扫码…,有些是收款…,所以需处理下。
2.华为和小米手机 通知获取 android.text 字段会抛出空指针异常(这是异常处理崩溃日志里面发现的,经过华为手机debug,一个通知,onNotificationPosted调用了两次)。
拿到想要的东西之后发送广播—>存储---->发送服务器。测试通知也匹配了自己的包名,然后弹出提示。

ensureCollectorRunning()这个方法是针对小米手机重启服务后接收不到通知的,即使重写了onRebind,也并没卵用,但我发现重置NotificationService,测试之后并没有得出是否真的有用,我只是用了服务绑定,给定两个服务相互协助,监听服务倒了,我用另一个服务重启监听服务的操作。在AndroidManifest.xml 配置两个服务即可。因为没有小米手机,这两个方案都是度娘找来的,没法测试哪个方案有了效果,测试过在监听类里面写生命周期,但是,即使杀死app,也未发现调用了onDestroy方法。

该demo只针对支付宝收款拦截,微信扫码收款也是一样的道理,但是,如果借助微信小账本收款则拦截不到金额的问题,我已研究出来了,由于是公司的产品,市场上还没有这样的需求,不影响公司的业务,无可奉告,请谅解!

最后打一个小广告,喜欢网购的朋友,下载这个app能领优惠券在淘宝天猫购买商品,还能返现提现,最近网购比较多,觉得挺好用,记得填我的邀请码:YMLVEBO,唔该嗮。
在这里插入图片描述

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值