• 随手翻译的文档,要看懂linux内核,内核附带的文档自然是逃不过😂,全英文配合Google翻译也得上。。。。

  • 最后更新时间 10/05/2007

  • linux看门狗设备API

  • Copyright 2002 Christer Weingel wingel@nano-system.com

  • 本文档的一些内容来自 sbc60xxwdt ,版权属于 Copyright 2000 Jakob Oestergaard jakob@ostenfeld.dk

  • 本文档基于linux内核 2.4.18


介绍

  • 你应该已经知道了,看门狗定时器(WDT)是一个硬件复位电路,在系统软件出现故障时复位系统。
  • 通常,用户空间的定期喂狗程序通过/dev/watchdog的设备文件通知内核看门狗驱动程序。驱动收到通知,会告知硬件看门狗一切正常,硬件看门狗需要等一会再复位系统。如果用户空间通知失败(RAM错误、内核漏洞等等),通知将停止。超时后,硬件看门狗将复位系统(重启)。
  • linux看门狗的API是相当特殊的结构,不同的驱动程序对API的执行各不相同,有时会碰到不兼容的情况。本文档参数记录现有的情况,以供未来的驱动开发参考。

最简单的API

  • 所有驱动都支持的基本操作模式。/dev/watchdog被打开后,看门狗被激活并复位系统,除非在一小段时间内被ping通?(喂狗),这段时间被称为超时时间或间断时间。简单的喂狗的方式是向驱动程序写入一些数据,一个非常简单的实例在see samples/watchdog/watchdog-simple.c下.
  • 更高级的驱动程序可以做到例如检查一个http服务器,在执行喂狗操作之前,做出回应。
  • /dev/下设备节点被关闭后,看门狗也同时被禁用,除非支持“Magic Close”(见下文),这并不是个好办法。因为如果看看门狗demo有bug导致系统崩溃,但系统不会重启。因为这样一些驱动支持”Disable watchdog shutdown on close”, CONFIG_WATCHDOG_NOWAYOUT”这样的配置选项。一旦编译内核时候启用这些选项,一旦看门狗程序激活,就不可能禁用看门狗。如果看门狗demo崩溃,系统会在超时后自动重启。看门狗驱动通常也支持nowayout module parameter,以便在运行时控制nowayout module。

Magic Close feature

  • 如果驱动程序支持Magic Close,那么设备将不支持禁用看门狗,除非在关闭设备文件前,向/dev/watchdog下设备文件写入特定的magic 字符 ‘V’。如果用户空间的 daemon关闭了看门的设备文件,但是没有发送那个特定的字符,驱动会认为daemon进程已经被杀,并且停止了定期喂狗行为。这样会导致超时后系统重启。

ioctl API

  • 所有的驱动程序都支持ioctl API

  • 喂狗操作使用ioctl完成

  • 所有的驱动都要支持一个KEEPALIVE的ioctl命令,这个命令可以起到向驱动写入数据一样的作用。所以watchdog daemon的主函数循环可以这样实现

    1
    2
    3
    4
    while (1) {
    ioctl(fd, WDIOC_KEEPALIVE, 0);
    sleep(10);
    }

Setting and getting the timeout

  • 某些驱动支持运行时通过SETTIMEOUT ioctl命令修改超时时间,这些驱动都有WDIOF_SETTIMEOUT的标志位_。超时时间是一个单位为秒的整数,驱动程序会返回这个变量实际设置的数值,但是由于硬件限制,返回的数值可能与设置数值不同。

    1
    2
    3
    int timeout = 45;
    ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
    printf("The timeout was set to %d seconds\n", timeout);
  • 这个实例或许会输出”The timeout was set to 60 seconds”,如果设备超时时间为分钟。

  • 自从2.4.18内核开始,可以使用GETTIMEOUT ioctl命令,获取超时时间。

    1
    2
    ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
    printf("The timeout was is %d seconds\n", timeout);

Pretimeouts

  • 一些看门狗定时器可以在系统重启前一段时间设置一个触发器。这样运行linux在系统从前记录一些重要的信息(像panic信息和kernel coredumps),具体可以通过NMT、中断等机制实现。

    1
    2
    pretimeout = 10;
    ioctl(fd, WDIOC_SETPRETIMEOUT, &pretimeout);
  • 请注意,pretimeout 时间是在 超时关闭系统 之前的秒数。不是直到发生pretimeout 事件的秒数。例如设定的超时时间是60s, pretimeout是10s。pretimeout事件会在50s时发生。pretimeout设置为0代表禁用pretimeout。

  • 同样存在一个获取pretimeout的ioctl命令。

    1
    2
    ioctl(fd, WDIOC_GETPRETIMEOUT, &timeout);
    printf("The pretimeout was is %d seconds\n", timeout);
  • 不是所有看门狗驱动都支持 pretimeout

获取重启前秒数

  • 一些看门狗驱动支持获取系统重启前秒数。通过WDIOC_GETTIMELEFT ioctl_命令可以返回系统重启前秒数。

    1
    2
    ioctl(fd, WDIOC_GETTIMELEFT, &timeleft);
    printf("The timeout was is %d seconds\n", timeleft);

环境监测

  • 所有的看门狗驱动都被要求返回更多关于系统最后一次重启的信息,像是温度、风扇转速、电源等。GETSUPPORT ioctl 的命令可以返回 设备可以支持的信息。

    1
    2
    struct watchdog_info ident;
    ioctl(fd, WDIOC_GETSUPPORT, &ident);
  • 返回的结构ident中的字段如下:

    1
    2
    3
    4
    5
    identity  描述watchdog driver的字符串

    firmware_version the firmware version of the card if available
    options 设备支持情况的标志位

  • options描述了GET_STATUS 和GET_BOOT_STATUS ioctl命令_可以返回那些信息。并且可以设置。()**??无法理解什么意思??**

  • WDIOF_OVERHEAT cpu过热复位_
    系统重启因为,超过了温度限制上限。

  • WDIOF_FANFAULT 风扇失效复位_

  • WDIOF_EXTERN1 External relay 1_
    外部监控电源1被触发,???Controllers intended for real world applications include external monitoring pins that will trigger a reset.??(不明白什么意思)

  • WDIOF_EXTERN2 External relay 2
    外部监控电源2被触发

  • WDIOF_POWERUNDER 电源坏了,电源带不动了
    机器显示欠压。


  • 后面一点不翻了,有时间再添坑

  • WDIOF_CARDRESET Card previously reset the CPU
    The last reboot was caused by the watchdog card

  • WDIOF_POWEROVER Power over voltage
    The machine is showing an overvoltage status. Note that if one level is under and one over both bits will be set - this may seem odd but makes sense.

  • WDIOF_KEEPALIVEPING Keep alive ping reply
    The watchdog saw a keepalive ping since it was last queried.

  • WDIOF_SETTIMEOUT Can set/get the timeout
    The watchdog can do pretimeouts.

  • WDIOF_PRETIMEOUT Pretimeout (in seconds), get/set
    For those drivers that return any bits set in the option field, the GETSTATUS and GETBOOTSTATUS ioctls can be used to ask for the current status, and the status at the last reboot, respectively.

    1
    2
    3

    int flags;
    ioctl(fd, WDIOC_GETSTATUS, &flags);

    or

    1
    ioctl(fd, WDIOC_GETBOOTSTATUS, &flags);
  • Note that not all devices support these two calls, and some only support the GETBOOTSTATUS call.

  • Some drivers can measure the temperature using the GETTEMP ioctl. The returned value is the temperature in degrees fahrenheit.

    1
    2
    int temperature;
    ioctl(fd, WDIOC_GETTEMP, &temperature);
  • Finally the SETOPTIONS ioctl can be used to control some aspects of the cards operation.

    1
    2
    int options = 0;
    ioctl(fd, WDIOC_SETOPTIONS, &options);
  • The following options are available:

    • WDIOS_DISABLECARD Turn off the watchdog timer
    • WDIOS_ENABLECARD Turn on the watchdog timer
    • WDIOS_TEMPPANIC Kernel panic on temperature trip

  • 转载自”Blog of UnicornX” (http://unicornx.github.io/)

  • 面向对象的思想在c语言中的应用,第一次看到才知道c语言还能这么用。。。


前言

  • 语言只是工具,重要的是思想。但是!!C语言用面向对象??看了内核代码,真给那些写Linux、内核的大神跪了。。。
  • 当然因为C语言的局限性,并非完全面向对象的实现,私有成员等就没有了。

封装

  • 既类在C语言中的实现,实际上是 struct + 函数体 = 函数表
    结构体里不允许存在成员函数,但是换成指向函数的指针是可以滴。

  • 示例

    1
    2
    3
    4
    5
    struct foo_operations {
    void (*op_a) (struct foo *, loff_t, int);
    void (*op_b) (struct foo *, char __user *, size_t, loff_t *);
    void (*op_c) (struct foo *, const char __user *, size_t, loff_t *);
    };

继承

  • 这个简单点, struct 里面套 struct ,警告熟悉c语言的应该都清除。。。这里没有父类子类的严格区分,实际上是完全自由的。

  • 示例

    1
    2
    3
    4
    5
    6
    struct foo {
    int a;
    int b;
    int c;
    struct foo_operations ops;
    };

多态

  • 这个是最有意思的,c语言中严格规定同一个.c文件/工程中不能存在重名函数,但是c语言的精华-指针给了思路,借助结构体,我函数名不变,每指向不同的结构体变量就行了。。。虽然还是很麻烦。。

  • 示例

    1
    2
    3
    4
    5
    6
    struct foo a;
    a->ops = ops1;
    a->ops->op_a(a);
    //切换
    a->ops = ops2;
    a->ops->op_a(a);

链表

  • 内核内大量运用链表,开始不清楚真是头晕。。

  • 示例

    1
    2
    3
    struct list_head {
    struct list_head *next, *prev;
    };

    一个指向自身的结构体,包含下一个与自身的指针。
    使用

    1
    2
    3
    4
    struct A_LIST {
    data_t data;
    struct list_head *list;
    };
  • offsetof宏

    1
    #define offsetof(type, member) (size_t)&(((type*)0)->member)

    offsetof是用来判断结构体中成员的偏移位置

  • container_of_宏

    1
    2
    3
    #define container_of(ptr, type, member) ({ \
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \
    (type *)( (char *)__mptr - offsetof(type,member) );})

    根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针..链表元素常用


资料来源如下

  • 乐联网

编程环境

  • Android Studio 2.2.3

最终效果

  • 使用okhttp上传数据。
  • Tcp长连接实现方向控制
  • 以代码为主

相关教程

  • okhttp入门

    http://blog.csdn.net/biezhihua/article/details/50603624

  • 乐联网

    https://www.lewei50.com/dev/doc/176
    https://www.lewei50.com/dev/doc/155

  • Tcp长连接

    http://ls15114843569.blog.51cto.com/11015399/1767195

  • 简易上传

    http://ls15114843569.blog.51cto.com/11015399/1767195

上传数据

  • API介绍

    https://www.lewei50.com/dev/apiinfo/3

  • API测试

    https://www.lewei50.com/dev/apitest/3

  • 地址:http://www.lewei50.com/api/v1/gateway/updatesensors/你的网关号

    POST方式

    需要配置header头 Userkey

  • 数据发送/返回方式JSON

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [
    {
    "Name":"T1",
    "Value":"1"
    },
    {
    "Name":"01H1",
    "Value":"96.2"
    }
    ]

    返回格式

    1
    2
    3
    4
    {
    "Successful": true,
    "Message": null
    }
  • okhttp POST 传感器数据 这里使用了一个静态内部类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    	//返回数据处理
    public okhttp3.Callback callback = new okhttp3.Callback() {
    @Override
    public void onResponse(Call call, Response response) throws IOException {
    //返回服务器内容
    String responsedata = response.body().string();
    LogUtil.d("okHttp", responsedata);
    }

    @Override
    public void onFailure(Call call, IOException e) {
    //异常处理
    LogUtil.d("okHttp", "POST错误");
    }
    };

    //内部类
    public static class Http {
    public static final MediaType MEDIA_TYPE_MARKDOWN
    = MediaType.parse("text/x-markdown; charset=utf-8");
    //POST数据,指定接收回掉
    public static void postData(String sensor_name, String sensor_data, okhttp3.Callback callback) {
    OkHttpClient client = new OkHttpClient();
    final String value =
    "[" +
    " {" +
    " \"Name\":\"" + sensor_name + "\"," +
    " \"Value\":\"" + sensor_data + "\"" +
    " }" +
    "]";
    RequestBody requestBody = new RequestBody() {
    @Override
    public MediaType contentType() {
    return MEDIA_TYPE_MARKDOWN;
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
    sink.write(value.getBytes());
    }
    };
    Request request = new Request.Builder()
    .url("http://www.lewei50.com/api/V1/gateway/UpdateSensors/01")
    .header("userkey", "你的userkey")
    .post(requestBody)
    .build();
    client.newCall(request).enqueue(callback);
    }
    }
  • 实际使用 放在一个后台服务内,调用相当简单

    1
    Http.postData("PM2.5", "你的数据转为String", callback);

    Tcp长连接,远程控制

  • 首先参考乐联网反向控制教程,新建一个控制器,这里以开关为例。

  • 原理是与服务器保持一个TCP长连接,不到40s刷新一次,以此保持通道,与被控制段通信,发送控制信息。

  • Tcp长连接参考了@墨迹流韶的Android基于Tcp协议的Socket长链接封装

  • 地址 tcp.lewei50.com
    端口号 9960
    心跳包间隔 1min以内

  • 发送/接收数据格式 Json
    本地发送数据格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {

    "method": "update",

    "gatewayNo": "你的网关号",

    "userkey": "你的userkey"

    }&^!

    服务器发送控制命令格式,数据处理时需要去掉字符串最后的&^!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    {

    "method":"send",

    "gatewayNo":"01",

    "userkey":"6d16ddb3c58c4e448a7e15e7acxxxxxx",

    "f":" updateSensor",

    "p1":"D1",

    "p2":"1"

    }&^!

    本地响应控制命令后返回数据格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {

    "method":"response",

    "result":{

    "successful":true,

    "message":"ok!",

    "data":[{

    "id":"D1",

    "value":"1"

    },

    }&^!
  • TCP连接类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    public class TcpSocketHelper {
    private String mTag = "TcpSocketHelper";
    private Socket socket;
    private boolean _connect;
    private ReceiveThread mReceiveThread;
    private boolean receiveStop;
    private Date lastKeepAliveOkTime;
    private OnRecivedListener mRecivedListener;
    //地址
    private String mIpAddr = "http://tcp.lewei50.com";
    //端口
    private int mPort = 9960;

    /**
    * 开启链接socket
    * @param ipAddr
    * @param port
    */
    public void startConnect(String ipAddr, int port){
    LogUtil.d(mTag, "准备链接...");
    this.mIpAddr = ipAddr;
    this.mPort = port;
    InetAddress serverAddr;
    try {
    socket = new Socket(ipAddr, port);
    LogUtil.d(mTag, "准备链接...");
    _connect = true;
    mReceiveThread = new ReceiveThread();
    receiveStop = false;
    mReceiveThread.start();
    LogUtil.d(mTag, "链接成功...");

    } catch (Exception e) {
    LogUtil.d(mTag, "链接出错..." + e.getMessage());
    e.printStackTrace();
    }
    }

    /**
    * 关闭链接
    */
    public void closeConnect(){
    if (socket != null){
    try {
    socket.close();
    socket = null;
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    /**
    * 保持心跳
    */
    public void KeepAlive() {
    // 判断socket是否已断开,断开就重连
    if (lastKeepAliveOkTime != null) {
    LogUtil.d(mTag, "上次心跳成功时间:"+ DateFormat.format("yyyy-MM-dd HH:mm:ss", lastKeepAliveOkTime));
    Date now = Calendar.getInstance().getTime();
    long between = (now.getTime() - lastKeepAliveOkTime.getTime());// 得到两者的毫秒数
    if (between > 60 * 1000) {
    LogUtil.d(mTag, "心跳异常超过40,重新连接:");
    lastKeepAliveOkTime = null;
    socket = null;
    }

    } else {
    lastKeepAliveOkTime = Calendar.getInstance().getTime();
    }

    if (!checkIsAlive()) {
    LogUtil.d(mTag, "链接已断开,重新连接.");
    startConnect(mIpAddr, mPort);
    }
    }

    /**
    * 此方法是检测是否连接
    * @return
    */
    public boolean checkIsAlive() {
    if (socket == null||!socket.isConnected())
    return false;
    return true;

    }

    /**
    * 发送数据的方法
    * @param msg
    */
    public void sendmessage(String msg) {
    boolean isAlive = checkIsAlive();
    if (!isAlive)
    return;
    LogUtil.d(mTag, "准备发送消息:" + msg);
    try {
    if (socket != null && socket.isConnected()) {
    if (!socket.isOutputShutdown()) {

    //2.得到socket读写流
    OutputStream os=socket.getOutputStream();
    //true:是否自动flush
    PrintWriter outStream=new PrintWriter(os, true);
    outStream.print(msg);
    outStream.flush();
    }
    }
    LogUtil.d(mTag, "发送成功!");
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    /**
    * 设置接收数据监听器
    * @param mRecivedListener
    */
    public void setmRecivedListener(OnRecivedListener mRecivedListener) {
    this.mRecivedListener = mRecivedListener;
    }

    /**
    * 数据接收线程
    */
    class ReceiveThread extends Thread{

    @Override
    public void run() {
    while (true) {
    try {
    sleep(2000);
    // 判断 Socket 是否处于连接状态
    if (socket != null && socket.isConnected()) {
    // 客户端接收服务器端的响应,读取服务器端向客户端的输入流
    InputStream isRead = socket.getInputStream();
    // 缓冲区
    byte[] buffer = new byte[isRead.available()];
    // 读取缓冲区
    isRead.read(buffer);
    // 转换为字符串
    String responseInfo = new String(buffer);
    // 日志中输出
    if(responseInfo != null&&!responseInfo.equals("")){
    LogUtil.d("TcpManager", "返回:"+responseInfo);
    mRecivedListener.onRecived(responseInfo);
    }

    lastKeepAliveOkTime = Calendar.getInstance().getTime();
    KeepAlive();

    continue;
    } else {
    if (socket != null)
    LogUtil.d(mTag, "链接状态:" + socket.isConnected());
    }

    } catch (Exception e) {
    LogUtil.d(mTag, "监听出错:" + e.toString());
    e.printStackTrace();
    }
    }
    }
    }
    }
  • 使用,包装在一个后台service中,在service中实现TcpSocketHelper的onRecived方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      //tcp返回信息
    @Override
    public void onRecived(String data) {
    LogUtil.d("okHttpService", data);
    //处理服务器发回的数据
    }

    TcpSocketHelper tcpSocketHelper = new TcpSocketHelper();
    tcpSocketHelper.startConnect("tcp.lewei50.com", 9960);
    //设置监听
    tcpSocketHelper.setmRecivedListener(this);
  • 发送心跳包

    1
    2
    3
    4
    5
    6
    7
    8
    String value =
    " {" +
    " \"method\":\"update\"," +
    " \"gatewayNo\":\"01\"," +
    " \"userkey\":\"你的userkey\"" +
    " }&^!";
    //发送数据
    tcpSocketHelper.sendmessage(value);
  • 处理数据 在service的onRecived中

  • 本地处理完毕后,向服务器返回被控制器状态

    1
    2
    3
    String value5 = "{\"method\":\"response\",\"result\":{\"successful\":true,\"message\":\"ok!\",\"data\":[{\"id\":\"D1\",\"value \":\"1\"}]}}&^! ";

    tcpSocketHelper.sendmessage(value6);


  • 资料来源如下
  • 第一行代码(第二版)

Activity基础

Activity定义

  • Activity 是Android四大组件之一,用户可与其提供的屏幕进行交互,以执行操作。 每个 Activity 都会获得一个用于绘制其用户界面的窗口。窗口可以充满屏幕,也可浮动。应用通常由多个彼此联系的 Activity 组成。应用中的某个 Activity 为“主”Activity,即首次启动应用时的Activity。

创建Activity

  • 新建FirstAtivity继承自AppCompatActivity

    1
    2
    3
    4
    5
    public class FirstActivity extends AppCompatActivity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    } }
  • 创建加载布局
    1.jpg

  • 切换到first-layout

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
    android:id="@+id/button_1"
    android:text="button_1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
    </LinearLayout>
  • 在FirestActivity中加载layout
    在onCreate中加入

    1
    setContentView(R.layout.first_layout);
  • 在AndroidMainfest文件中注册。

    1
    2
    3
    4
    5
    6
    7
    8
    <activity
    android:name=".FirstActivity"
    android:label="This is FirstActivity">
    <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    </activity>
  • 在模拟器中启动apk
    5ea429.jpg

使用Toast

Toast google官方说明

  • 推送一个短小信息推送给用户
    如图(摘自android developer)
    toast.png

  • 使用方法

    1
    2
    Toast.makeText(context, text, duration).show();

    举例

    1
    2
    Toast.makeText(FirstActivity.this, "you clicked button 1", Toast.LENGTH_SHORT).show();
    /* activity.this 消息内容 显示时间设置 */

使用Menu

  • 创建菜单

    • 在res下新疆menu文件夹,右击menu文件夹—new—Menu resource file,创建main的菜单文件。
    • main.xml中添加如下代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
       <?xml version="1.0" encoding="utf-8"?>
      <menu xmlns:android="http://schemas.android.com/apk/res/android">
      <item
      android:id="@+id/add_item"
      android:title="Add"/>
      <item
      android:id="@+id/remove_item"
      android:title="Remove"/>
      </menu>
      这里创建了两个菜单项 Add和Remove
    • 在FirestActivity中重写 onCreateOptionsMenu()方法(快捷键 Ctrl+O)
      1
      2
      3
      4
      5
        @Override
      public boolean onCreateOptionsMenu(Menu menu) {
      getMenuInflater().inflate(R.menu.main, menu);
      return true;
      }
      getMenuInflater()可以得到MenuInflater对象,再调用.inflate就可以创建菜单。.inflat接受俩个参数,一是资源文件名,二是菜单项添加至那个对象中。onCreateOptionsMenu方法中返回true表示创建菜单并显示。
    • 效果如下。
      Screenshot_1481113497.png
  • 创建菜单点击响应事件

    • 重写onOptionsItemSelected方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Override
      public boolean onOptionsItemSelected(MenuItem item) {
      switch (item.getItemId()) {
      case R.id.add_item:
      Toast.makeText(this, "Add", Toast.LENGTH_SHORT).show();
      break;
      case R.id.remove_item:
      Toast.makeText(this, "Remove", Toast.LENGTH_SHORT).show();
      break;
      default:
      }
      return true;
      }
      通过item.getItemId()判断点击选项,弹出不同的Toast

向一个activity 并传递字符串

  • 构建一个Intent
    Intent intent = new Intent(this, DisplayMessageActivity.class);
    构造方法有两个参数:
    Context 是第一个参数,这里使用了this是因为Activity是Context的子类。
    Class 类是系统将要分发的APP组件,在这里,这个Activity将会被启动。
  • ```java
    public void sendMessage(View view) {
      // 创建一个新的intent对象,绑定DisplayMessageActivity
      Intent intent = new Intent(this, DisplayMessageActivity.class);
      //创建一个editText对象,绑定xml中editText
      EditText editText = (EditText) findViewById(R.id.edit_message);
      //获取editText中输入文字,转成字符串
      String message = editText.getText().toString();
      //一个Intent对象可以携带被称为extras的键值对。
      // putExtra()方法将键放在第一个参数中,将值放在第二个参数中。
      intent.putExtra(EXTRA_MESSAGE, message);
      //启动intent对应Activity
      startActivity(intent);
      }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    * ```java
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_display_message);

    //创建本地intent
    Intent intent = getIntent();
    //取出String类型数据,如果是Int类型就是getIntExtra,Boolean类型就是getBooleanExtra
    String message = intent.getStringExtra(MainActivity.EXTRA_MESSAGE);
    //显示String
    //创建新的TextView
    TextView textView = new TextView(this);
    //设置文本大小
    textView.setTextSize(40);
    //设置显示内容
    textView.setText(message);
    //绑定xml
    ViewGroup layout = (ViewGroup) findViewById(R.id.activity_display_message);
    //ViewGroup中添加TextView
    layout.addView(textView);
    }

返回数据给上一个Activity

  • 构建Intent使用startActivityForResult()方法传入请求码,启动下一个Activity
    在下一个Activity中构建Intent,intent.putExtra存入键值-key,调用setResult()方法,传入finish()结束掉Activity
    重写第一个Activity中的onActivityResult()方法,调用.getStringExtra取出key对应键值。

  • startActivityForResult(Intent, int Bundle) Intent与单纯启动Activity的Intent相同,第二个是请求码,下一级 回调提供相同的请求码,以便您应用可以正确识别结果。

    1
    2
    Intent intent = new Intent(FirestActivity.this,ScendActivity.class);
    startActivityForResult(intent,1);
  • setResult()方法,第一个参数向上级方法处理结果,一般使用RESULT_OK或RESULT_CANCELED,第二个参数 对应Intent

    1
    2
    3
    4
    5
    6
    7
    //新建显示Intent
    Intent intent = new Intent();
    //存入key-键值
    intent.putExtra("data_return","Hello Firest");
    setResult(RESULT_OK,intent);
    //结束Activity
    finish();
  • onActivityResult()三个参数.第一个startActivityForResult() 传递的请求代码。第二个 Activity 指定的结果代码。成功是 RESULT_OK;失败,则是 RESULT_CANCELED。第三个是传送结果数据的 Intent。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     @Override
    protected void onActivityResult(int requestCode,int resultCode,Intent data){
    //选择不同请求码对应处理逻辑
    switch(requestCode){
    case 1:
    //处理结果时候ok
    if(resultCode == RESULT_OK){
    //取出数据
    String returnedData = data.getStringExtra("data_return");
    Log.d("FirstActivity", returnedData);
    }
    }
    }

Activity生命周期

basic-lifecycle.png

  • Activity 3种状态
    • Resumed/继续
      Activity 处于前台,且用户可以与其交互
    • Paused/暂停
      Activity 被在前台中处于另一个 Activity—部分阻挡。 暂停的 Activity 不会接收用户输入并且无法执行任何代码。
    • Stopped/停止
      Activity完全隐藏,对用户完全不可见.当停止时,activity的所有状态信息比如成员变量都会被保留,但是不能再执行任何代码

启动一个Activity

  • onCreate方法
  • 主Activity:
    • 用户点击app图标,启动主Activity
    • 在AndroidManifest.xml中声明
      1
      2
      3
      4
      <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
  • 通过调用onCreate方法创建一个Activity实例,onCreate方法在Activity的整个生命周期中只执行一次。
  • 之后系统会很快的调用onStart和onResume方法,Activity进入Resumed/继续模式。直到被其他activity覆盖/屏幕关闭。

    销毁Activity

  • onDestory方法
    大多数的APP不需要实现这个方法,因为本地类引用会随着Activity一起总结,不过Activity的清理工作应该放在onPause下或者onStop。
  • Note:
    在所有的情况下系统调用onDestory方法之后已经调用过onPause方法与onStop方法,不过有一个例外情况:你在onCreate方法中调用了finish方法。在一些例子中,当你的Activity临时决定要启动另一个Activity,你可能要在onCreate方法内调用finish方法来销毁这个Activity,在这种情况下,系统会立即调用onDestory方法,而不会调用其它任何生命周期方法。

暂停Activity

  • onPause()方法
  • Activity被其他Activity覆盖/失去用户焦点,系统调用onPause()方法,Activity 进入暂停状态。
    • note:android7.0及以上版本加入了多窗口模式,当Activity失去用户焦点时,可能处于多窗口模式。
  • onPause() 常用回调:
    • 检查 Activity 是否可见。不可见则停止可能消耗 CPU 的操作
    • 提交未保存的更改,仅保存用户离开时希望永久性保存此类更改(比如电子邮件草稿)。
    • 释放系统资源,GPS/Camer等
    • 示例 (释放Camer)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      public void onPause() {
      super.onPause(); // Always call the superclass method first

      // Release the Camera because we don't need it when paused
      // and other activities might need to use it.
      if (mCamera != null) {
      mCamera.release();
      mCamera = null;
      }
    • 注意事项:
      • 在onPause()一般不执行永久性存储用户更改,不执行 CPU 密集型工作,这些工作一般放在onStop() 。

继续 Activity

  • onResume() 方法
  • 暂停状态回到继续状态,Activity第一次启动时也会调用这个方法。
  • onResume() 以初始化在 onPause() 期间释放的组件。
  • 示例(重新获取Camera)
    1
    2
    3
    4
    5
    6
    7
    public void onResume() {
    super.onResume(); // Always call the superclass method first

    // Get the Camera instance as the activity achieves full user focus
    if (mCamera == null) {
    initializeCamera(); // Local method to handle camera init
    }

停止 Activity

  • note:
    大多数相对简单的 Activity 而言,系统在 Activity 停止时会将Activity 实例保留在系统内存中,无需实现 onStop() 和 onRestart() 或甚至onStart() 方法。可能只需使用 onPause() 暂停正在进行的操作,并从系统资源断开连接。
  • onStop() 方法

  • 场景:

    • 用户在最近应用切换到另一个应用
    • 应用中执行开始新 Activity 的操作
    • Activity使用时,接打电话
  • 调用时,Activity不再可见,释放几乎所有用户不使用时不需要的资源。如果系统内存紧张,则可能销毁内存中的Acitivity实例。

  • onStop() 方法调用后,Activity不再可见,极端情况下,系统可能会仅终止应用进程,而不调用 onDestroy() ,因此需要使用 onStop() 释放几乎所有用户不使用时不需要的资源。

  • 尽管 onPause() 方法在 onStop()之前调用,在onStop() 执行更大、占用更多 CPU 的关闭操作,比如向数据库写入信息

  • 示例(草稿笔记内容保存在永久存储)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      protected void onStop() {
    super.onStop(); // Always call the superclass method first

    // Save the note's current draft, because the activity is stopping
    // and we want to be sure the current note progress isn't lost.
    ContentValues values = new ContentValues();
    values.put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText());
    values.put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle());

    getContentResolver().update(
    mUri, // The URI for the note to update.
    values, // The map of column names and new values to apply to them.
    null, // No SELECT criteria are used.
    null // No WHERE columns are used.
    );
    }

启动/重启 Activity

  • onStart() 方法
  • Activity 停止转换为继续状态时,系统回调onRestart() 方法+ onStart() 方法.onStop() 方法清理了所有 Activity 的资源,重启 Activity 需要重新实例化它们。同时 Activity 初次创建时重新实例化它们。 出于此,经常使用 onStart() 方法作为 onStop() 方法的对应
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     @Override
    protected void onStart() {
    super.onStart(); // Always call the superclass method first

    // The activity is either being restarted or started for the first time
    // so this is where we should make sure that GPS is enabled
    LocationManager locationManager =
    (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);

    if (!gpsEnabled) {
    // Create a dialog here that requests the user to enable GPS, and use an intent
    // with the android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS action
    // to take the user to the Settings screen to enable GPS when they click "OK"
    }
    }

    @Override
    protected void onRestart() {
    super.onRestart(); // Always call the superclass method first

    // Activity being restarted from stopped state
    }

    保存 Activity 状态

  • onSaveInstanceState()方法
  • 默认情况下,Activity 实例被销毁时系统会使用 Bundle 实例状态保存 Activity 布局中有关每个 View 对象的信息。在Activity 重建时,布局状态便自动恢复先前的状态。
  • 默认实现保存有关 Activity 视图层次的状态信息,例如 EditText 小部件中的文本或ListView 的滚动位置
  • 要恢复的更多信息,需要重写 onSaveInstanceState()方法,将键值对添加至 Bundle 对象
  • basic-lifecycle-savestate.png
  • note:
    旋转屏幕时,Activity 将被销毁并重新创建。原因:方向更改时可能需要时加载备用资源(比如布局)
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    static final String STATE_SCORE = "playerScore";
    static final String STATE_LEVEL = "playerLevel";
    ...

    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current game state
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
    savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(savedInstanceState);
    }

恢复 Activity

  • onCreate() 和 onRestoreInstanceState() 回调方法均接收包含实例状态信息的相同 Bundle
  • onCreate() 方法
  • 调用onCreate() 方法需要区分是创建 Activity 的新实例还是恢复先前的实例,判断 Bundle 是否为 null
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); // Always call the superclass first

    // Check whether we're recreating a previously destroyed instance
    if (savedInstanceState != null) {
    // Restore value of members from saved state
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
    } else {
    // Probably initialize members with default values for a new instance
    }
    ...
    }
  • onRestoreInstanceState()方法
  • 只需要恢复的已保存的状态
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
     public void onRestoreInstanceState(Bundle savedInstanceState) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState);

    // Restore state members from saved instance
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
    }

重启Activity其他选择

  • 官方说明
  • 重启应用并恢复大量数据不仅成本高昂,而且会留下糟糕的使用体验,有两个其他选择

    在配置变更期间保留对象

  • Activity 因配置变更而重启,则可通过保留 Fragment 来减轻重新初始化 Activity 的负担
  • 当 Android 系统因配置变更而关闭 Activity 时,不会销毁已标记为要保留的 Activity 的片段。 您可以将此类片段添加到 Activity 以保留有状态的对象。

Activity最佳实践

知晓当前运行的活动

  • 自定义BaseActivity继承自AppCompatActivity
    重写onCreate方法,打印当前运行的Activity名
    app类所有Activity改为继承BaseActivity

  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    public class BaseActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //打印当前类名
    Log.d("BaseActivity", getClass().getSimpleName());
    }
  • 效果如下
    ScreenShot_20161208172740.png

随时退出程序

  • 新建ActivityCollector类作为活动管理器,添加/删除Activity登记,在BaseActivity中添加对应代码。
  • 代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
     public class ActivityCollector {
    //新建Activity的列表对象
    public static List<Activity> activities = new ArrayList<>();
    //添加Activity进入列表
    public static void addActivity(Activity activity) {
    activities.add(activity);
    }
    //在列表中移除Activity
    public static void removeActivity(Activity activity) {
    activities.remove(activity);
    }
    //for循环列表 结束所有Activity
    public static void finishAll() {
    for (Activity activity : activities) {
    if (!activity.isFinishing()) {
    activity.finish();
    }
    }
    }
    }
  • BaseActivity
    在onCreate()方法中添加
    1
    2
    //Activity管理器中添加新的活动
    ActivityCollector.addActivity(this);
    重写onDestroy()方法
    1
    2
    3
    4
    5
    6
    @Override
    protected void onDestroy(){
    super.onDestroy();
    //Activity管理器中移除活动
    ActivityCollector.removeActivity(this);
    }
  • 在任何地方调用finishAll()方法即可。

启动活动的最近写法

  • 启动下一个Activity并传递数据模块化
  • 在SecondActivity内增加actionStart()方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     //调用的Activity,数据1,数据2
    public static void actionStart(Context context, String data1, String data2){
    //新建Intent,绑定SecondActivity
    Intent intent = new Intent(context,ScendActivity.class);
    //存入data1,data2
    intent.putExtra("param1",data1);
    intent.putExtra("param2",data2);
    //启动Activity
    context.startActivity(intent);
    }
  • 启动SecondActivity方式
    ScendActivity.actionStart(FirestActivity.this,"data1","data2");


资料来源如下

  • 网络

编程环境

  • Android Studio 2.2.3

导语

  • 转眼刚用上手的MVP又不符合需求了,还是总结一下,继续MVVM吧。

  • 主要内容是转载,搭建小工程应该足够了。
    Android mvp 架构的自述
    如何更高效的使用MVP以及官方MVP架构解析

  • 框架开源在GitHub 地址点我直达

  • 好,我们开始吧!


MVC MVP MVVM

  • 省略200行,详情看Android App的设计架构:MVC,MVP,MVVM与架构经验谈
  • 再累赘属于掉书袋了,作者写的是很用心,恩,MVVM也有。

框架详解

  • 整个框架截图
    ScreenShot_20170509215208.png

  • 整体层次比较明确了,BaseClass 里存放基类,Model层存放数据存储有关类、Presenter层存放逻辑代码、View层存放Activity、frament等。

  • 这个框架是由MVP在Android中应用存在的问题而搭建的,基类中代码也由此而来。

问题

  • presenter一直持有Activity对象导致的内存泄漏问题

    • 使用mvp的时候,presenter会持有view,如果presenter有后台异步的长时间的动作,比如网络请求,这时如果返回退出了Activity,后台异步的动作不会立即停止,这里就会有内存泄漏的隐患。
    • 需要一个销毁view的方法,这里是在基类中完成的。
  • presenter绑定和解绑繁琐

    • 一般一个presenter对应一个Activity,一般应用内存在多个Actiivity,绑定与解绑相当繁琐。
    • 统一在 Basepresenter 与 BaseActivity中进行,同时解决内存泄漏问题,还有其他常用内容一并添加。
  • presenter 与 Model 的通信问题。

    • presenter已经与View层 强耦合了,框架中需要解耦presenter 与 Model ,使用异步通信。Handle等都太复杂了。。。
    • 开始没有什么好的解决办法,直到遇到了EventBus,当然EventBus也不是万能解决方案,跨进程,还是要另辟蹊径,不过EventBus足够自己写着完了。还有Rxjava,恩,还在玩着,没搞太懂。

代码

  • BasePresenter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     public abstract class BasePresenter<Viewinterface> {
    //传入泛型
    public Viewinterface mView;
    //绑定
    public void attach(Viewinterface mView) {
    this.mView = mView;
    }
    //解绑,防止view为空是内存泄漏
    public void dettach() {
    mView = null;
    }
    }
  • BaseActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
     public abstract class BaseActivity <Viewinterface,mPresenter extends BasePresenter<Viewinterface>>extends AppCompatActivity {
    //获取Presenter对象
    public mPresenter mpresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //Presenter实例化
    mpresenter = initPresenter();
    //打印当前activity
    LogUtil.d("Activity", getClass().getSimpleName()+"onCreate");
    }

    @Override
    protected void onResume() {
    //重新刷新时重新绑定view
    mpresenter.attach((Viewinterface) this);
    super.onResume();
    LogUtil.d("Activity", getClass().getSimpleName()+"onResume");
    }

    @Override
    protected void onDestroy() {
    //解绑presenter持有的view
    mpresenter.dettach();
    super.onDestroy();
    LogUtil.d("Activity", getClass().getSimpleName()+"onDestroy");
    }


    public abstract mPresenter initPresenter();
    }
  • Presenter

    1
    2
     public interface Presenter {
    }
  • mPresenter

    1
    2
    3
    4
    5
    6
    7
    8
     public class mPresenter extends BasePresenter<Viewif> implements Presenter {
    private Model mModel;

    public mPresenter(Viewif view) {
    this.attach(view);
    this.mModel = new mModel();
    }
    }
  • BaseView

    1
    2
    3
     public interface BaseView<T>  {

    }
  • Viewif

    1
    2
    3
     public interface Viewif extends BaseView {

    }
  • MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     public class MainActivity extends BaseActivity<Viewif, mPresenter> implements Viewif{

    Presenter presenter;
    //Presenter初始化
    public mPresenter initPresenter() {
    return new mPresenter(this);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    //presenter初始化
    presenter = mpresenter;
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }
    }
  • LogUtil
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
     public class LogUtil {
    public static final int VERBOSE = 1;//啰嗦,等级最低的
    public static final int DEBUG = 2;//调试
    public static final int INFO = 3;//信息
    public static final int WARN = 4;//警告
    public static final int ERROR = 5;//错误
    public static final int NOTHING = 6;//什么也不打印出来
    public static final int level = VERBOSE;//LEVEL:标准

    public static void v(String tag, String msg) {
    if (level <= VERBOSE) {//如果大于或者等于定义的标准就打印出来
    Log.v(tag, msg);
    }
    }

    public static void d(String tag, String msg) {
    if (level <= DEBUG) {
    Log.d(tag, msg);
    }
    }

    public static void i(String tag, String msg) {
    if (level <= INFO) {
    Log.i(tag, msg);
    }
    }

    public static void w(String tag, String msg) {
    if (level <= WARN) {
    Log.w(tag, msg);
    }
    }

    public static void e(String tag, String msg) {
    if (level <= ERROR) {
    Log.e(tag, msg);
    }
    }
    }


资料来源如下

  • 第一行代码(第二版)

编程环境

  • Android Studio 2.2.3

导语

  • 毕设中的蓝牙部分,记录以供复习

概述

最终效果

  • Android应用 与 Hc-05 蓝牙模块连接,单片机与 Android 端 可以通过串口正常收发数据.

  • 预留足够灵活的接口, Android 应用中可以在任意位置获取到蓝牙数据

  • Activity切换时,蓝牙连接不断开.

用到的开源库/知识点资料 简介/来源

  • 蓝牙:
    Android API 指南

    https://developer.android.com/guide/topics/connectivity/bluetooth.html

  • EventBus:
    EventBus-GitHub 是一个Android端优化的publish/subscribe消息总线,用来替代 Intent 、 Handler 、 Broadcast 等在 Actvity 、 Fragment 、 Service 等组件之间传递信息 . EventBus 可以传递一个完整的对象,简单高效, 注意 EventBus 只能在多线程之间传递消息,无法在不同进程之间传递消息 ,EventBus 3.0 以后进一步简化了传递方式,真的是很值得学习的一个开源库!

    参考资料如下:

    EventBus 3.0初探: 入门使用及其使用 完全解析

    EventBus3(3.0.0)源码解析


基础部分

  • 蓝牙 与 EventBus 基础部分

BlueTooth


  • 备注 : 这里使用的是 传统蓝牙 即 蓝牙4.0以前版本, 而不是 蓝牙4.0 ( ble低功耗蓝牙)及以后版本

  • Android 对 Bluetooth 做了很好的封装,我们可以比较轻松的在 Android Bluetooth API 上进行开发.
  • Android中所有蓝牙API均来自 android.bluetooth 包

BlueTooth基础

  • BluetoothAdapter 本地蓝牙适配器
    BluetoothAdapter 是所有蓝牙交互的入口点, 在初始化蓝牙及蓝牙配对阶段使用
    1.发现其他蓝牙设备
    2.查询绑定(配对)设备的列表
    3.使用已知的 MAC 地址实例化 BluetoothDevice
    4.创建 BluetoothServerSocket 侦听来自其他设备的通信。

  • BluetoothDevice 远程蓝牙设备
    含有该设备的信息,例如设备的名称、地址、类和绑定状态等。

  • BluetoothSocket 蓝牙套接字接口(与 TCP Socket 相似)
    与 BluetoothDevice 配合建立远程连接,允许应用通过 InputStream 和 OutputStream 与其他蓝牙设备交换数据.

初始化蓝牙

  • 声明蓝牙权限
    Android 应用使用蓝牙前都需要声明蓝牙权限
    一般只需要 BLUETOOTH 权限即可,但是考虑到之后有更多的需求,需要更改系统蓝牙设置,由此需要 BLUETOOTH_ADMIN 权限_也一起声明

    1
    2
    3
    4
    5
    <manifest ... >
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    ...
    </manifest>
  • 蓝牙设备可用性 获取 BluetoothAdapter
    获取 BluetoothAdapter,需要静态 getDefaultAdapter() 方法。getDefaultAdapter() 会返回一个表示设备自身的蓝牙适配器的 BluetoothAdapter 设备不支持蓝牙 则返回 null

    1
    2
    3
    4
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
    // 蓝牙不可用操作
    }
  • 开启蓝牙
    BluetoothAdapter 的 .isEnabled() 可以判断系统蓝牙是否开启.
    没有开启时 startActivityForResult() 发送包含 ACTION_REQUEST_ENABLE 的 Intent 请求系统开启蓝牙,并在 onActivityResult() 返回的数据中 RESULT_CANCELED 表示开启失败 RESULT_OK 表示开启成功,这里我们只检测开启失败情况,并提示用户

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_CANCELED) {
    Toast.makeText(MyApplication.getContext(),R.string.Bluetooth_openfail, Toast.LENGTH_SHORT).show();
    }
    BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
    //设备不支持蓝牙时处理
    }
    }
  • 查询已配对的设备
    调用 getBondedDevices(),返回已配对设备的一组 BluetoothDevice.之后使用for循环遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
    if (pairedDevices.size() > 0) {
    //重新加载bluetoothDeviceList
    bluetoothDeviceList.clear();
    //BluetoothDevice列表循环
    for (BluetoothDevice device : pairedDevices) {
    bluetoothDeviceList.add(device);
    }
    }
  • 扫描设备
    查找设备是非常耗费系统资源的事项,需要安排在子线程中执行,这里没有用到,由需要请参考Google官方蓝牙教程

  • 连接设备
    蓝牙分主从机,链接为服务器/客户端。这里使用的是连接为客户端。

    • 首先获取远程设备的 BluetoothDevice 对象,即在选择设备阶段的BluetoothDevice
    • 调用 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket,UUID通用唯一识别码 在蓝牙中具体是什么没有很好的解释。这里的值取的是
      "00001101-0000-1000-8000-00805F9B34FB"
    • 调用 connect() 发起连接,阻塞调用,需要在子线程中执行。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      //蓝牙连接子线程
      private class ConnectThread extends Thread {
      private final BluetoothSocket mmSocket;

      //解析函数
      ConnectThread(BluetoothDevice bluetoothDevice) {
      // 使用一个中间变量 tmp
      // mmSocket 类型是 final

      BluetoothSocket tmp = null;
      // 获取 BluetoothSocket
      try {
      // MY_UUID is the app's UUID string, also used by the server code
      tmp = bluetoothDevice.createRfcommSocketToServiceRecord(UUID.fromString(MyApplication.MY_UUID));
      } catch (IOException ignored) {
      }
      //赋值给 mmSocket
      mmSocket = tmp;
      }

      public void run() {
      // 关闭蓝牙扫描
      MyApplication.getBluetoothAdapter().cancelDiscovery();
      try {
      // 通过 socket 连接到设备. connect() 会一直执行直到成功连接或者抛出异常
      mmSocket.connect();
      } catch (IOException connectException) {
      //无法连接到蓝牙,关闭连接并退出
      try {
      mmSocket.close();
      }
      //没有正常关闭
      catch (IOException ignored) {
      }
      return;
      }
      // Do work to manage the connection (in a separate thread)
      mConnectedThread = new ConnectedThread(mmSocket);
      mConnectedThread.start();
      }

      /**
      * 关闭蓝牙连接
      */
      void cancel() {
      try {
      mmSocket.close();
      } catch (IOException ignored) {
      }
      }
      }
  • 管理连接
    获取BluetoothSocket
    获取 InputStream 和 OutputStream, getInputStream() 和 getOutputStream() 来处理数据传输。read(byte[]) 和 write(byte[]) 读取数据并写入到流式传输。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    private class ConnectedThread extends Thread {
    //BluetoothSocket
    private final BluetoothSocket mmSocket;
    //输入流
    private final InputStream mmInStream;
    //输出流
    private final OutputStream mmOutStream;

    ConnectedThread(BluetoothSocket socket) {
    //传入BluetoothSocket,实例化mmSocket
    mmSocket = socket;
    //输入/输出流 中间变量
    InputStream tmpIn = null;
    OutputStream tmpOut = null;

    // 输入输出流实例化
    try {
    tmpIn = socket.getInputStream();
    tmpOut = socket.getOutputStream();
    } catch (IOException e) {
    e.printStackTrace();
    }
    mmInStream = tmpIn;
    mmOutStream = tmpOut;
    }

    public void run() {
    byte[] buffer = new byte[1024];
    int bytes;
    // 连接成功时
    while (true) {
    try {
    // 在InputStream读数据
    bytes = mmInStream.read();
    //发送数据
    Events.bluetooth_Recycle bluetooth_recycle = new Events.bluetooth_Recycle();
    bluetooth_recycle.s = bytes;
    bluetooth_recycle.bytes = String.valueOf((char) bytes);
    EventBus.getDefault().post(bluetooth_recycle);
    } catch (IOException e) {
    break;
    }
    }
    }

    //写方法
    void write(byte[] bytes) {
    try {
    mmOutStream.write(bytes);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    //关闭流连接
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException e) {
    }
    }
    }

    EventBus

  • 直接放链接了

    http://www.jianshu.com/p/a040955194fc
    http://www.jianshu.com/p/acfe78296bb5
    http://www.ff50.net/view/40565212977623506063.html

正文

  • 蓝牙连接子线程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
     private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;

    //解析函数
    ConnectThread(BluetoothDevice bluetoothDevice) {
    // 使用一个中间变量 tmp
    // mmSocket 类型是 final

    BluetoothSocket tmp = null;
    // 获取 BluetoothSocket
    try {
    // MY_UUID is the app's UUID string, also used by the server code
    tmp = bluetoothDevice.createRfcommSocketToServiceRecord(UUID.fromString(MyApplication.MY_UUID));
    } catch (IOException ignored) {
    }
    //赋值给 mmSocket
    mmSocket = tmp;
    }

    public void run() {
    // 关闭蓝牙扫描
    MyApplication.getBluetoothAdapter().cancelDiscovery();
    try {
    // 通过 socket 连接到设备. connect() 会一直执行直到成功连接或者抛出异常
    mmSocket.connect();
    } catch (IOException connectException) {
    //无法连接到蓝牙,关闭连接并退出
    try {
    mmSocket.close();
    }
    //没有正常关闭
    catch (IOException ignored) {
    }
    return;
    }
    // Do work to manage the connection (in a separate thread)
    mConnectedThread = new ConnectedThread(mmSocket);
    mConnectedThread.start();
    }

    /**
    * 关闭蓝牙连接
    */
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException ignored) {
    }
    }
    }
  • 管理连接子线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
      //管理连接子线程
    private class ConnectedThread extends Thread {
    //BluetoothSocket
    private final BluetoothSocket mmSocket;
    //输入流
    private final InputStream mmInStream;
    //输出流
    private final OutputStream mmOutStream;

    ConnectedThread(BluetoothSocket socket) {
    //传入BluetoothSocket,实例化mmSocket
    mmSocket = socket;
    //输入/输出流 中间变量
    InputStream tmpIn = null;
    OutputStream tmpOut = null;

    // 输入输出流实例化
    try {
    tmpIn = socket.getInputStream();
    tmpOut = socket.getOutputStream();
    } catch (IOException e) {
    e.printStackTrace();
    }
    mmInStream = tmpIn;
    mmOutStream = tmpOut;
    }

    public void run() {
    byte[] buffer = new byte[1024];
    int bytes;
    // 连接成功时
    while (true) {
    try {
    // 在InputStream读数据
    bytes = mmInStream.read();
    //发送数据
    Events.bluetooth_Recycle bluetooth_recycle = new Events.bluetooth_Recycle();
    bluetooth_recycle.s = bytes;
    bluetooth_recycle.bytes = String.valueOf((char) bytes);
    EventBus.getDefault().post(bluetooth_recycle);
    } catch (IOException e) {
    break;
    }
    }
    }

    //写方法
    void write(byte[] bytes) {
    try {
    mmOutStream.write(bytes);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

    //关闭流连接
    void cancel() {
    try {
    mmSocket.close();
    } catch (IOException e) {
    }
    }
    }
  • 连接蓝牙

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //连接线程
    private ConnectThread mConnectThread;
    //管理连接进程
    private ConnectedThread mConnectedThread;

    //创建连接子线程
    mConnectThread = new ConnectThread(bluetoothDevice);
    //启动连接子线程
    mConnectThread.start();
  • 向蓝牙写入数据

    1
    2
    3
    4
    5
    6
    7
     //发送数据String,        主线程            优先级4       非粘性事件
    @Subscribe(threadMode = ThreadMode.MAIN, priority = 4, sticky = false)
    public void onEvent(Events.bluetooth_Send bluetoothSend) {
    String bytes = bluetoothSend.bytes;
    mConnectedThread.write(bytes.getBytes());
    LogUtil.d(Tag, "onEvent:Send" + bluetoothSend.bytes);
    }
  • 接收数据处理

    1
    2
    3
    4
    5
    6
      //数据转换                后台进程            优先级3       非粘性事件
    @Subscribe(threadMode = ThreadMode.BACKGROUND, priority = 3, sticky = false)
    public void onEvent(Events.bluetooth_Transformers bluetoothTransformers) {
    int bytes = bluetoothTransformers.bytes;
    //处理数据
    }


资料来源如下

  • 第一行代码(第二版)
  • Android Training Notifying the User
  • Android API 通知
  • 【Android】状态栏通知Notification、NotificationManager详解

编程环境

  • Android Studio 2.2.3

导语

  • OneTapDoze遇到的第一个难题,顺带记录Android 通知相关内容

最终效果

  • 绿色守护进入doze模式后,会在通知栏创建一个计时通知 如下图
    Screenshot_20170307-231118.png

  • 退出doze模式后,会指示进入doze和退出doze的时间段。
    Screenshot_20170307-231132.png

  • 我们要仿照的样式就是这样,进入退出doze,对应创建通知的代码都在doze模式改变的Broadcast中。

基础部分

创建通知

  • 行文前:
    Android每次版本几乎都会有通知API的改动,不同版本之间通知兼容性很是问题,为此我们使用support_v7 库中的NotificationCompat.Builder代替Notification.Builder二者使用方式相同

  • 涉及到的两个类

    • NotificationManager
    • Notification (support库中对应NotificationCompat)
  • NotificationManager

    • 状态栏通知的管理类,负责发通知、清除通知等
    • NotificationManager 是一个系统Service,必须通过 getSystemService()方法来获取
  • Notification

    • 具体的状态栏通知对象,可以设置icon、文字、提示声音、振动等参数
    • 一个Builder对象至少包含三个方面
      • 一个小图标,通过setSmallIcon()方法设置。
      • 通知标题,通过setContentTitle()方法设置。
      • 详细文本,通过setContentText()方法设置。
  • 简单示例

    • 创建通知构建器
      代码

      1
      2
      3
      4
      5
      6
      Notification notification = new NotificationCompat.Builder(this)
      .setContentTitle("这是通知标题")
      .setContentText("这是通知内容")
      //这里使用的是应用图标,一般没人这么干,就是为了方便
      .setSmallIcon(R.mipmap.ic_launcher)
      .build();
    • 发布通知

      • 获得NotificationManager的实例
      • 使用notify()方法发布通知。在调用notify()方法 指定通知的ID,(ID用于通知更新) 加载Notification实例
      1
      2
      3
      4
      int mNotificationId = 001;

      NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
      manager.notify(mNotificationId, notification);
  • 效果如下

    • android 7.0 / 6.0 /4.0 通过
    • ScreenShot_20170202173430.png

更新通知

  • 可以创建一个全新的NotificationCompat对象,也可以在原NotificationCompat对象基础上修改,最后只要 .notify 方法中对应同一个 mNotificationId ,系统就会自动更新已有通知,代码不在累赘。

  • 基础部分到此,足矣

正题

  • 实现时间段的显示,肯定会保存系统进入doze 退出doze对应的时间点。再计算出中间经历的时间。

提取系统时间

  • java.util.Date
    这里用到了java中的 时间类型 java.util.Date
    java.util.Date 是java中常用时间类型,可以被SimpleDateFormat格式化format() 指定输出的时间格式
    比如我们需要 小时:分:秒
    SimpleDateFormat scanf = new SimpleDateFormat("HH:mm:ss");
    scanf.format(java.util.Date) 即可。

  • 提前当前系统时间

    1
    2
    java.util.Date time_after = null;
    time_after = new java.util.Date(System.currentTimeMillis());
  • 计算时间差
    理论上是两个时间相减再格式化输出即可,可惜没成,原因还没找到。于是土办法:

    • 调用 .getTime() 方法,将 java.util.Date 转化为毫秒计时(long)
    • 取两者差值
    • 转化为 时间长短 字符串返回,
  • 代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
     size = (time_after.getTime() - time_befor.getTime());
    string = scanf.format(time_befor) + "-" + scanf.format(time_after) + "="+ trform(size);

    String trform(long size) {
    long day, hour, min, secone;

    day = size / (1000 * 60 * 60 * 24); //以天数为单位取整
    hour = (size / (60 * 60 * 1000) - day * 24); //以小时为单位取整
    min = ((size / (60 * 1000)) - day * 24 * 60 - hour * 60); //以分钟为单位取整
    secone = (size / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - min * 60);

    if (day != 0) {
    return day + "d" + hour + "h" + min + "m" + secone + "s";
    } else if (hour != 0) {
    return hour + "h" + min + "m" + secone + "s";
    } else if (min != 0) {
    return min + "m" + secone + "s";
    } else {
    return secone + "s";
    }
    }

通知扩展布局

  • 在如下图片中,可以发现,通知在发出后,只默认显示最近一次的时间记录,其他时间记录可以在此条通知上下滑查看。明显与基础部分不同。
    Screenshot_20170307-231132.png

  • 此处应用了 Builder.setStyle() 拓展布局,顾名思义,这是用于拓展通知显示范围


使用扩展布局

  • 使用拓展布局
    • 构建一个 inboxStyle 对象
    • .addLine方法 添加 String inboxStyle 行
    • 使用 Builder.setStyle( inboxStyle )加载到通知
    • 等待通知发布即可。
  • 简单例程如下
    1
    2
    3
    4
    5
    6
    //创建 inboxStyle
    NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
    //添加 String 行 ,前提是 我已经创建了一个 String
    inboxStyle.addLine(string);
    // 在notification 设置 inboxStyle 在一堆 build 里面
    .setStyle(inboxStyle)
  • 清除原有内容,发布新的 inboxStyle 只需要 inboxStyle = new NotificationCompat.InboxStyle();

Doze模式改变广播

  • 对应 ACTION_DEVICE_IDLE_MODE_CHANGED

  • 坑:只能动态注册,静态注册无效,具体代码中powermanger.ACTION_DEVICE_IDLE_MODE_CHANGED 需要实例化 PowerManger ,而 PowerManger 又是与 Activity绑定,所以只有应用保持后台存活时才会进入广播。
  • 例程如下
    1
    2
    3
    4
    5
    6
    7
    8
    //实例化 PowerManager
    PowerManager powermanger = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
    //动态注册广播
    intentFilter = new IntentFilter();
    intentFilter.addAction(powermanger.ACTION_DEVICE_IDLE_MODE_CHANGED);
    //idlemodechage 是 BroadcastReceiver
    idlemodechage = new IdleModeChange();
    registerReceiver(idlemodechage, intentFilter);


资料来源如下

  • 第一行代码(第二版)
  • android6.0运行时权限详解

编程环境

  • Android Studio 2.2.3

导语

  • 内容提供器是安全的应用间共享数据途径,在正式进入之前需要了解一下 Android 自6.0以来权限的变化

Android 权限详解


资料来源:

  • android6.0运行时权限详解
  • Develop-API Guides-系统权限
  • 第一行代码(第二版)

Android权限介绍

  • Android 是一个权限分隔的操作系统
    每个应用系统标识(Linux 用户 ID 和组 ID)不同。系统各部分标识亦不相同。Linux 据此将不同的应用以及应用与系统分隔开来。
    更详细的安全功能通过“权限”提供,权限 会限制特定进程可以执行的具体操作,并且根据 URI 权限授权临时访问特定的数据段

  • 权限声明
    最常见的权限声明在 AndroidManifest.xml 文件中一个或多个uses-permission 标记,

    • 例如 声明短信权限
      1
      2
      3
      4
      5
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.android.app.myapp" >
      <uses-permission android:name="android.permission.RECEIVE_SMS" />
      ...
      </manifest>
  • android 6.0 权限机制
    android 6.0之前 权限及在安装的时候,根据权限声明产生一个权限列表,用户只有在同意之后才能完成app的安装。
    android 6.0 以后 引入了 运行时权限 功能,应用所需权限不再一次性在安装时申请,而是在运行时需要用到那种权限向用户申请,同时用户也可以随时取消该授权

  • 权限分类

    • Normal Permissions(普通权限)
      不涉及用户隐私,不需要用户进行授权的,例如 手机震动、访问网络等
    • Dangerous Permission(危险权限)
      涉及到用户隐私的,需要用户进行授权,例如 读取sdcard、访问通讯录等
      危险权限组如表1

应用运行时申请权限(适用于 Android6.0 及以上版本)

  • 通过一个简单的实例来说明过程

    实例:

  • 拨打电话 CALL_PHONE _权限为例

    • 权限声明
      1
      <uses-permission android:name="android.permission.CALL_PHONE"/>
  • 拨打10010

    • 代码如下 构建一个隐式intent 启动拨打电话
      1
      2
      3
      Intent intent = new Intent(Intent.ACTION_CALL);
      intent.setData(Uri.parse("tel:10010"));
      startActivity(intent);
  • 在主界面上添加一个Button 在其onClick方法中拨打电话,运行程序!


  • 在An’d’roid 6.0 以下版本中 电话都可以正常拨出。Android 6.0 以上版本中 则可能会应用崩溃/无反应,查看logcat的打印日志可以看到 “Permission Denial”权限被禁止的信息,接下来尝试使用 运行时申请权限

  • 检查是否已获得授权
    int checkSelfPermission(Context context, String permission)方法

    • Context context context
      String permission 权限具体名称
      打电话对应 Manifest.permission.CALL_PHONE _
    • 方法返回 PERMISSION_GRANTED / PERMISSION_DENIED
    • 例程
      1
      2
      3
      4
      5
      6
      7
      8
      if(ContextCompat.checkSelfPermission
      (MainActivity.this,Manifest.permission.CALL_PHONE)
      !=PackageManager.PERMISSION_GRANTED)
      {
      ;
      }else {
      ;
      }
  • 申请权限
    void ActivityCompat.requestPermissions (Activity activity, String[] permissions, int requestCode)方法

    • Activity activity Activity实例
      String[] permissions 请求权限名
      int requestCode 请求码,大于0即可,对应在onRequestPermissionsResult 回掉方法中的requestCode

    • 例程

      1
      2
      ActivityCompat.requestPermissions(MainActivity.this,
      new String[]{Manifest.permission.CALL_PHONE},1 );
  • 授权后回掉
    无论用户是否授权都会进入回掉方法,需要重写该方法。
    void onRequestPermissionsResult (int requestCode, String[] permissions, int[] grantResults)方法

    • int requestCode 在 requestPermissions 中传入的请求码
      String[] permissions 请求的权限,不为空
      int[] grantResults 授权结果 只有 PERMISSION_GRANTED / PERMISSION_DENIED 两个值

    • 例程

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Override
      public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
      switch (requestCode) {
      //请求码 1
      case 1:
      //授权结果(int) 的 长度是否大于0 与上 授权结果是否等于 PackageManager.PERMISSION_GRANTED
      if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
      //授权通过执行
      } else {
      //授权没有通过
      }
      break;
      default:
      }
      }
  • 当用户拒绝后再次询问
    boolean ActivityCompat.shouldShowRequestPermissionRationale(Activity activity, String permission) (真心太长了)

    • Activity activity Activity实例
      String permission 权限名称
      应用第一次需要授权时,用户授权,该方法返回false。当用户拒绝授权,下次需要该权限时,该方法会返回true
    • 例程
      1
      2
      3
      4
      5
      6
      7
       if (ActivityCompat.shouldShowRequestPermissionRationale
      (PermissionActivity.this,Manifest.permission.READ_CONTACTS))
      {
      //第二次询问用户是否授权
      }else{
      //用户依旧拒绝后操作
      }
  • 例程源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class MainActivity extends AppCompatActivity {

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

    public void Button(View view){
    if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE},1 );
    }else {
    call();
    }

    }

    private void call() {
    try {
    Intent intent = new Intent(Intent.ACTION_CALL);
    intent.setData(Uri.parse("tel:10010"));
    startActivity(intent);
    } catch (SecurityException e) {
    e.printStackTrace();
    }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
    case 1:
    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    call();
    } else {
    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
    }
    break;
    default:
    }
    }

    }
    1
    2
    3
    4
    5
    <Button
    android:onClick="Button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!" />


资料来源如下

  • 第一行代码(第二版)

编程环境

  • Android Studio 2.2.3

Android 中的主要数据存储

  • 有三种

  • 保存键值集
    要保存的相对较小键值集合,使用 SharedPreferences

  • 保存文件
    使用 Android 文件系统通过 File API 读取和写入文件,适合按开始到结束的顺序不跳过地读取或写入大量数据。 例如,图片文件或通过网络交换的任何内容

  • 在 SQL 数据库中保存数据
    Android内建的SQLite 数据库存储重复或结构化数据


  • PS:这里如果没有学过 SQL 数据库使用,推荐LitePal
  • Android开源数据库LitePal :郭霖(第一行代码作者)
  • 详细介绍在这里(懒人必备)Android开源数据库LitePal

保存键值集

  • 想要保存的相对较小键值集合

  • 调用SharedPreferences

  • 首先:获取SharedPreferences的句柄
    用户可以创建一个共享参数文件或访问一个已经存在的共享参数文件。
    具体途径有三条

    • getSharedPreferences (String name, int mode)
      • 第一个参数:SharedPreferences的名称,第二个:参数指定操作模式
      • 需要创建/读取 多个共享参数文件时使用,每个文件都拥有一个标识符,可以通过这个标识符通过该方法的第一个参数获得共享参数对象。可以通过APP中的任意Context对象调用它。
      • 示例
        1
        2
        //指定SharedPreferences名称为data  模式为MODE_PRIVATE:只有当前程序才有权限读取/修改
        SharedPreferences sharedPref = getSharedPreferences("data", MODE_PRIVATE);
    • getPreferences (int mode)
      • 参数:指定操作模式
      • 只创建 Activity 的一个SharedPreferences。在Activity 中使用。 方法会检索属于该 Activity 的默认共享首选项文件,无需提供名称
      • 示例
        1
        2
        //模式:MODE_PRIVATE:只有当前程序才有权限读取/修改
        SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
    • getDefaultSharedPreferences(context)
      • 参数:context
      • 自动以档期应用程序包名作为前缀命名SharedPreferences文件
  • 写入数据到共享参数中
    • SharedPreferences 调用 edit() 创建一个 SharedPreferences.Editor
      使用诸如 putInt() 和 putString() 方法写入的键和值
      最后调用commit() 以保存更改
      也可调用apply() 保存更改

  • commit() 与 apply() 相同/区别
  • 相同
    • 都是提交preference修改数据
  • 区别
    • apply没有返回值 commit返回boolean表明修改是否提交成功
    • apply是将操作提交到内存,而后异步真正提交到文件
      commit是同步的提交到文件
      多个commit同时提交,互相等待,效率较低
      apply直接覆盖,效率较高
    • apply没有任何错误提示,只是提交的动作

  • 示例
    1
    2
    3
    4
    5
    6
    //创建 .edit()
    SharedPreferences.Editor editor = sharedPref.edit();
    //写入字符串。key-值 TextView
    editor.putString("TextView","hello");
    //保存更改
    editor.commit();
  • 从共享参数中读取数据
    • 获取SharedPreferences的句柄
      方式同上
    • 调用比如getInt()或getString()方法,然后传入键值,如果键不存在,则会返回一个默认值
    • 示例
      1
      2
      3
      4
      5
      6
      //获取SharedPreferences的句柄
      SharedPreferences sharedPreferences = getSharedPreferences("data",MODE_PRIVATE);
      //读取键值对应数据
      String text = sharedPreferences.getString("TextView","");
      //打印到log中 验证
      Log.d("data",text);

保存到文件

  • Android 设备有两个文件存储区域: 内部存储 和 外部存储
    Android 设备早期内置的非易失性内存(内部存储),以及移动存储介质 微型 SD 卡等(外部存储)
    任何Android设备始终有两个存储空间,并且无论外部存储设备是否可移动,API 的行为均一致

  • Android的文件存储是以java流为基础,并优化了一些操作。java流不属于本文范围。

  • 涉及java流部分参考Android文件IO详解

内部存储

保存到内部存储

  • 某一应用对其内部存储 始终具有 进行读写的权限,而无需声明任何权限,在 Android N之前,内部文件可以通过放宽权限让其他应用访问。但是极不推荐以这种方式共享文件
  • 简单实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    public void save(String inputText){
    //新建一个FileOutputStream对象(字节流)
    FileOutputStream out = null;
    //新建一个BufferedWriter (字符流)
    BufferedWriter writer = null;
    try{
    //out 实例化
    out = openFileOutput("data",Context.MODE_PRIVATE);
    //OutputStreamWrit 字节流转换为字符流
    //字符流绑定BufferedWriter(缓冲区)
    writer = new BufferedWriter(new OutputStreamWriter(out));
    //写入到文件
    writer.write(inputText);
    }catch (IOException e ){
    e.printStackTrace();
    } finally {
    try{
    //如果写入成功
    if (writer != null) {
    //关闭流
    writer.close();
    }
    }catch (IOException e){
    e.printStackTrace();
    }
    }
    }
  • FileOutputStream:

    • 继承自 OutputStream 用于文件处理的节点流
    • 用于写入原始字节流到文件
  • FileOutputStream openFileOutput (String name, int mode)

    • 返回FileOutputStream对象
    • 第一个参数是文件名
    • 第二个参数是操作模式,有4种取值
      • MODE_PRIVATE(默认)_当同一文件名存在时覆盖原文件
      • MODE_APPEND _当同名文件存在时,在原文件末尾追加
      • 其余两种均在android4.2及以上版本中废弃
  • OutputStreamWrit

    • 继承自 Writer
    • 将字节流转换为字符流
  • BufferedWriter

    • 继承自Writer
    • 绑定字符输出流,提高具体的流对象的效率
    • .flush()方法 对缓冲区进行刷新,让数据到目的地
    • .close();方法 关闭缓冲区,即关闭绑定的流
从内部存储读取文件
  • 简单实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public String load(){
    //新建FileInputStream对象
    FileInputStream in = null;
    //新建 BufferedReader对象
    BufferedReader reader = null;
    //新建一个StringBuilder 空
    content = new StringBuilder();
    try{
    //FileInputStream对象实例化
    in = openFileInput("data");
    //字节流转换为字符流
    //缓冲流绑定字符流
    reader = new BufferedReader(new InputStreamReader(in));
    //空 String
    String line = "";
    //读取文件内容,并判断是否读取完成
    while((line = reader.readLine())!=null){
    //读取内容添加到 StringBuilder 对象中
    content.append(line);
    }
    }catch (IOException e){
    e.printStackTrace();
    }
    finally {
    if(reader != null){
    try{
    //关闭流
    reader.close();
    }catch (IOException e){
    e.printStackTrace();
    }
    }
    }
    //将读取结果 由StringBuilder转换为String 并返回
    return content.toString();
    }
  • FileInputStream

    • 继承自 InputStream,用于文件处理的节点流
    • 从文件中读取
  • FileInputStream openFileInput(String filename)

    • 返回FileInputStream对象
    • 参数为 读取的文件名
  • InputStreamRead

    • 将字节流转换为字符流
  • BufferedReader

    • 缓冲流

外部存储

  • 占坑


资料来源如下

  • 第一行代码(第二版)

编程环境

  • Android Studio 2.2.3

BroadcastReceiver基础

  • 首先祭出官方文档及中文翻译

    https://developer.android.com/reference/android/content/BroadcastReceiver.html
    http://www.jianshu.com/p/1b56172b0c77

BroadcastReceiver概述

  1. @别路寻忆
    Android中的四大组件是 Activity、Service、Broadcast和Content Provider。而Intent是一个对动作和行为的抽象描述,负责组件之间程序之间进行消息传递。那么Broadcast Receiver组件就提供了一种把Intent作为一个消息广播出去,由所有对其感兴趣的程序对其作出反应的机制。

  2. @zuolongsnail专栏

    • 广播接收器是一个专注于接收广播通知信息,并做出对应处理的组件。很多广播是源自于系统代码的──比如,通知时区改变、电池电量低、拍摄了一张照片或者用户改变了语言选项。应用程序也可以进行广播──比如说,通知其它应用程序一些数据下载完成并处于可用状态。

    • 应用程序可以拥有任意数量的广播接收器以对所有它感兴趣的通知信息予以响应。所有的接收器均继承自BroadcastReceiver基类。

    • 广播接收器没有用户界面。然而,它们可以启动一个activity来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

广播分类

  • 触发广播发生的事件分类有两种

    1. Android系统广播事件
      由android设备状态变化而触发的系统广播如:
      Intent.ACTION_POWER_CONNECTED;
      //插上外部电源时发出的广播
      Intent.ACTION_SCREEN_ON;
      //屏幕被打开之后的广播
      ACTION_TIME_CHANGED
      //系统时间改变而触发的广播
    2. 自定义的广播事件
      这个比较好理解了,比如qq当我们再另外一台手机上登陆时,手头的这个手机qq就会自动下线。腾讯的服务器通过后台服务启用了自定义广播来终结正在运行的Activity,详情之后会有例程,不再累赘。
  • 能够被接收的广播类型

    1. 普通广播(Normal broadcasts):
      (由 Context.sendBroadcast发出)异步发出。所有广播接收器都可以在同一时间接收广播。广播接收者无法接收广播的处理结果或者阻断广播的传递。

    2. 有序广播(Ordered broadcasts):
      (由 Context.sendOrderedBroadcast发出)每次只发送给一个广播接收器。当每个广播接收器依次执行时,它可以向下一个广播接收器传播结果,或者阻断该广播,使得该广播不能被下一个广播接收器接收到。

    • 通俗解释:
      普通广播就相当于小区/村委会的大喇叭,有事发生(触发广播)通知所有的人(广播接收器),全功率的大喊大叫确保所有人(广播接收器)都能听到。而有人(广播接收器)在睡觉被吵醒但是又没办法砸了那个大喇叭,只能继续听着(无法接收广播的处理结果或者阻断广播的传递)。
      有序广播 就相当于间谍机关的绝密消息传递。绝密到手(触发广播)间谍秘密汇总给上线A(广播接收器A)。本来 上线(广播接收器A)应该 听取完毕重新整理情向首长B(广播接收器B)报告(向广播接收器B传播 广播接收器A修改的结果),但是A被收买,将情报隐匿了,没有向首长B汇报(广播接收器A阻断该广播,使得该广播不能被下一个广播接收器B接收到)。(PS结局首长B错误带人炸了村委会,该睡觉的人终于可以安生睡觉了!)
  • 广播事件注册有两种

    1. 静态注册,就是在AndroidManifest.xml文件中定义,注册的广播接收器必须要继承BroadcastReceiver.
    2. 动态注册,是在程序中使用Context.registerReceiver注册,注册的广播接收器相当于一个匿名类。两种方式都需要IntentFIlter。
    • 例程见下节。

动态注册(在代码中注册)

  • 在代码中通过registerReceiver()注册。app关闭后,该接收器也会随之销毁。

  • 首先定义一个内部子类NetworkChangeReceiver继承自 BroadcastReceiver

    1
    2
    3
    4
    5
    6
    7
    8
    class NetworkChangeReceiver extends BroadcastReceiver {
    @Override

    //重写onReceive,接收到广播后提示消息
    public void onReceive(Context context, Intent intent) {
    Toast.makeText(context, "network is available网络已变化", Toast.LENGTH_SHORT).show();
    }
    }
  • 在onCreate()方法中创建IntentFilter实例和NetworkChangeReceiver实例。并在IntentFilter实例中添加网络变化时系统广播对应值。随后传入registerReceiver()中注册。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //创建IntentFilter实例
    intentFilter = new IntentFilter();
    //添加对应系统广播
    intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
    //添加NetworkChangeReceiver实例
    networkChangeReceiver = new NetworkChangeReceiver();
    //动态注册
    registerReceiver(networkChangeReceiver, intentFilter);
    }
  • 动态注册的广播接收器最后要取消注册。在onDestroy()方法中调用unregisterReceiver()销毁动态注册的广播接收器。

    1
    2
    3
    4
     protected void onDestroy(){
    super.onDestroy();
    unregisterReceiver(networkChangeReceiver);
    }
  • 最后要在AndroidMainfest.xml中声明查询系统网络状态的权限。

    1
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
  • 运行效果如如所示(模拟器)

静态注册(xml中注册)

  • 直接在Manifest.xml文件中配置广播接收者

  • 例程为了方便同样以android.net.conn.CONNECTIVITY_CHANGE为例,与动态注册相同。

    不使用内部子类(第一行代码)

  • 新建名称为BootCompleteReceiver的java class 代码如下

    1
    2
    3
    4
    5
    6
       public  class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent){
    Toast.makeText(context, "网络改变", Toast.LENGTH_SHORT).show();
    }
    }
  • 在AndroidManifest.xml文件中注册广播接收器
    代码如下

    1
    2
    3
    4
    5
    <receiver android:name=".BootCompleteReceiver">
    <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
    </intent-filter>
    </receiver>
  • 在AndroidManifest.xml中声明权限

    1
    2
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>

    使用内部子类(在Activity中定义)


在这晕圈了一下午,找不出毛病。看书多仔细吧,书上说了不用内部子类。
详细资料在这里

http://blog.csdn.net/chdjj/article/details/19496567


  • 清单文件注册广播接收者时,广播接收者的名字格式需要注意因为是内部类,所以需要在内部类所在的类与内部类之间加上$符号(这一点在AndroidStudio中输入时有提示)
  • 内部类在声明时一定要写成静态内部类(class关键字前加上static)。否则会抛出异常(广播发生时,应用停止运行)

  • 在MainActivity中新建子类

    1
    2
    3
    4
    5
    6
     public static class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent){
    Toast.makeText(context, "Boot complete", Toast.LENGTH_SHORT).show();
    }
    }
  • 在AndroidManifest.xml中注册广播接收器

    1
    2
    3
    4
    5
    <receiver android:name=".MainActivity$BootCompleteReceiver">
    <intent-filter>
    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
    </intent-filter>
    </receiver>
  • 在AndroidManifest.xml中声明权限

    1
    2
    <uses-permission android:name="android.permission.INTERNET"></uses-permission>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
  • 运行效果同动态注册

使用广播接收器注意事项

  • 在onReceive()方法中不宜添加过多逻辑/耗时操作,广播接收器没有多线程,一旦时间过长,程序就会报错。
  • 广播接收器一般为启动其他组件作用。

发送标准广播


  • 发送标准广播之前,首先要注册一个作为目标的广播接收器。(过程略,只上代码)
    新建MyBroadcastReceiver.class
    1
    2
    3
    4
    5
    6
    7
       public class MyBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent)
    {
    Toast.makeText(context,"MyBroadcastReceiver",Toast.LENGTH_SHORT).show();
    }
    }
    在Xml中注册(静态)
    1
    2
    3
    4
    5
    <receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
    <action android:name="com.example.broadcasttest.MyBroad"/>
    </intent-filter>
    </receiver>

  • xml中注册一个Button

    1
    2
    3
    4
    5
    <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="SendBroad"
    android:text="SendBroad" />
  • MainActivity中新建 SendBroad()函数
    首先构建一个 Intent对象,将自定义的广播值填入。再调用sendBroadcast方法将广播发送出去。

    1
    2
    3
    4
    5
        public void SendBroad(View view)
    {
    Intent intent = new Intent("com.example.broadcasttest.MyBroad");
    sendBroadcast(intent);
    }
  • 发送的是标准广播。运行效果如下图。
    Screenshot_20160910-220121.png

发送有序广播


  • 首先新建一个Broad2 的工程。同样接收Broad发送的广播。
    代码如下
    1
    2
    3
    4
    5
    6
    7
       public class AnotherBroadcast extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent)
    {
    Toast.makeText(context," AnotherBroadcast",Toast.LENGTH_SHORT).show();
    }
    }
    1
    2
    3
    4
    5
    <receiver android:name=".AnotherBroadcast">
    <intent-filter android:priority="10">
    <action android:name="com.example.broadcasttest.MyBroad"/>
    </intent-filter>
    </receiver>
  • 测试效果:当摁下发送广播按钮后,弹出两个提示。

  • 修改Broad项目中onClick对应事件。将sendBroadcast()改为sendOrderedBroadcast();发送有序广播。

    1
    2
    3
    4
    5
        public void SendBroad(View view)
    {
    Intent intent = new Intent("com.example.broadcasttest.MyBroad");
    sendOrderedBroadcast(intent,null);
    }
  • 效果与发送标准广播相同(还未定义优先级/截断等)

  • 定义优先级,再Broad的AndroidMainfest.xml中修改注册的广播添加android:priority=”100”优先级100

    1
    2
    3
    4
    5
    <receiver android:name=".MyBroadcastReceiver">
    <intent-filter android:priority="100">
    <action android:name="com.example.broadcasttest.MyBroad"/>
    </intent-filter>
    </receiver>
  • 在Broad2的AndroidMainfest.xml中添加android:priority=”10”优先级10

    1
    2
    3
    4
    5
    <receiver android:name=".AnotherBroadcast">
    <intent-filter android:priority="10">
    <action android:name="com.example.broadcasttest.MyBroad"/>
    </intent-filter>
    </receiver>
  • 发送广播后MyBroadcastReceiver最先收到广播。

  • 截断广播。有序广播中前一个广播接收器可以截断广播传播。添加 abortBroadcast();即可。

    1
    2
    3
    4
    5
    6
    7
    8
       public class MyBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent)
    {
    Toast.makeText(context,"MyBroadcastReceiver",Toast.LENGTH_SHORT).show();
    abortBroadcast();
    }
    }
  • 再次点击按钮发送广播,只有MyBroadcastReceiver可以接收到发送的广播。

本地广播

  • 只在app应用内部传递的广播。注册过程类似于动态注册。

  • 定义一个内部类LocalReceiver继承自BroadcastReceiver

    1
    2
    3
    4
    5
    6
        public static class LocalReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent){
    Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).show();
    }
    }
  • 首先在onCreate()方法中通过localBroadcastManager.getInstan得到一个LocalBroadcastManager的实例,再创建IntentFilter实例和LocalReceiver实例。并在IntentFilter实例中添加广播。随后传入localBroadcastManager.registerReceiver()中注册本地广播。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    private IntentFilter intentFilter;
    private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);

    localBroadcastManager = localBroadcastManager.getInstance(this);

    intentFilter = new IntentFilter();
    intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
    localReceiver = new LocalReceiver();
    localBroadcastManager.registerReceiver(localReceiver, intentFilter);
    }
  • 在Activity的onDestroy()中销毁注册。

    1
    2
    3
    4
    5
    @Override
    protected void onDestroy(){
    super.onDestroy();
    localBroadcastManager.unregisterReceiver(localReceiver);
    }
  • 在Button对应的函数中调用localBroadcastManager.sendBroadcast发送本地广播。

    1
    2
    3
    4
    5
        public void SendBroad(View view)
    {
    Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
    localBroadcastManager.sendBroadcast(intent);
    }
  • 效果基本同上,不加累赘。不过在Broad2中是怎样都搜不到广播了。

  • 本地广播特点

    • 明确广播只在应用内部,传递数据无需担心泄密。
    • 其他程序广播无法发送至程序内部。
    • 本地广播比全局广播更为高效。