广播机制概述

Android广播分为两个方面:广播发送者和广播接收者,通常情况下,BroadcastReceiver指的就是广播接收者。广播作为Android组件间的通信方式,可以使用的场景如下:
  • 1.同一app内部的同一组件内的消息通信(单个或多个线程之间);
  • 2.同一app内部的不同组件之间的消息通信(单个进程);
  • 3.同一app具有多个进程的不同组件之间的消息通信;
  • 4.不同app之间的组件之间消息通信;
  • 5.Android系统在特定情况下与App之间的消息通信。
从实现原理看上,Android中的广播使用了观察者模式,基于消息的发布/订阅事件模型。因此,从实现的角度来看,Android中的广播将广播的发送者和接受者极大程度上解耦,使得系统能够方便集成,更易扩展。具体实现流程要点粗略概括如下:
  • 1.广播接收者BroadcastReceiver通过Binder机制向AMS(Activity Manager Service)进行注册;
  • 2.广播发送者通过binder机制向AMS发送广播;
  • 3.AMS查找符合相应条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到BroadcastReceiver(一般情况下是Activity)相应的消息队列中;
  • 4.消息循环执行拿到此广播,回调BroadcastReceiver中的onReceive()方法。
对于不同的广播类型,以及不同的BroadcastReceiver注册方式,具体实现上会有不同。但总体流程大致如上。
由此看来,广播发送者和广播接收者分别属于观察者模式中的消息发布和订阅两端,AMS属于中间的处理中心。广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有无接收者接收,也不确定接收者到底是何时才能接收到。显然,整体流程与EventBus非常类似。
在上文说列举的广播机制具体可以使用的场景中,现分析实际应用中的适用性:
  • 第一种情形:实际应用中肯定是不会用到广播机制的(虽然可以用),无论是使用扩展变量作用域、基于接口的回调还是Handler-post/Handler-Message等方式,都可以直接处理此类问题,若适用广播机制,显然有些“杀鸡牛刀”的感觉,会显太“重”;
  • 第二种情形:对于此类需求,在有些较复杂的情况下单纯的依靠基于接口的回调等方式不好处理,此时可以直接使用EventBus等,相对而言,EventBus由于是针对统一进程,用于处理此类需求非常适合,且轻松解耦。
  • 第三四五种情形:由于涉及不同进程间的消息通信,此时根据实际业务使用广播机制会显得非常适宜。

APP退出后静态Receiver能否接收到广播的问题

我们可能一直有一个观点:静态注册的广播接收者即使app已经退出,只要有相应的广播发出,此广播接收者依然可以接收到广播。
但此种描述自Android 3.1开始对于系统广播不再成立了!

Android 3.1开始,系统在Intent中增加了与广播相关的flag参数,分别是FLAG_INCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES。
FLAG_INCLUDE_STOPPED_PACKAGES:包含已经停止的包(即包所在的进程已经退出)
FLAG_EXCLUDE_STOPPED_PACKAGES:不包含已经停止的包

主要原因是:
自Android3.1开始,系统本身增加了对所有app当前是否处于运行状态的跟踪。在发送广播时,系统默认直接增加了值为FLAG_EXCLUDE_STOPPED_PACKAGES的flag,导致即使是静态注册的广播接收者,对于其所在进程已经退出的app,同样无法接收到广播。
因此,对于系统广播,由于是系统内部直接发出的,无法更改此flag值,所以,3.1开始对于静态注册的接收系统广播的BroadcastReceiver,如果App进程已经退出,将不能接收到系统广播。
但是对于自定义的广播,由于我们可以手动设置此flag为FLAG_INCLUDE_STOPPED_PACKAGES,使得静态注册的BroadcastReceiver,即使所在App进程已经退出,也能能接收到广播,并会启动应用进程。

在3.1以前,相信不少app可能通过静态注册方式监听各种系统广播,以此进行一些业务上的处理。但3.1以后,静态注册接受广播方式的改变,将直接导致此类方案不再可行。于是,通过将Service与App本身设置成不同的进程已经成为实现此类需求的可行的替代方案。

MainActivity


public class MainActivity extends ListActivity {
    private TelephonyManager tm;
    private MyBRReceiver myReceiver;
    public static final String MY_BROADCAST_ACTION_UNORDERED = "com.bqt.broadcast.songwennuan_unordered";
    public static final String MY_BROADCAST_ACTION_ORDERED = "com.bqt.broadcast.songwennuan_ordered";
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String[] array = { "动态注册广播,监听网络状态变化", "取消注册广播", "发送自定义的无序广播", "发送自定义的有序广播" };
        ListAdapter mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new ArrayList<String>(Arrays.asList(array)));
        setListAdapter(mAdapter);
        tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
        tm.listen(new MyPhoneStateListener(this), PhoneStateListener.LISTEN_CALL_STATE);//权限 READ_PHONE_STATE
        //动态注册。需要程序启动才可以接收广播。一般在onCreate中注册,在onDestroy中取消注册。
        myReceiver = new MyBRReceiver();
        registerReceiver(myReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));//权限:ACCESS_NETWORK_STATE
    }
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        switch (position) {
        case 0://可以重复注册,不会有什么异常
            registerReceiver(myReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));//权限:ACCESS_NETWORK_STATE
            Toast.makeText(this, "已注册", Toast.LENGTH_SHORT).show();
            break;
        case 1:
            try {
                unregisterReceiver(myReceiver);//不管注册多少次,只能取消注册一次,若多次取消或在没有注册时取消会报IllegalArgumentException
                Toast.makeText(this, "已取消注册", Toast.LENGTH_SHORT).show();
            } catch (Exception e) {
                Toast.makeText(this, "异常啦!", Toast.LENGTH_SHORT).show();
            }
            break;
        case 2:
            sendUnOrderedBroadcast();
            break;
        case 3:
            sendOrderedBroadcast();
            break;
        }
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(myReceiver);//如果已取消注册或没有注册,会报错:Unable to destroy activity… : IllegalArgumentException: Receiver not registered…
    }
    //********************************************************************************************************************************
    //发送无序广播。不可被拦截,不可终止。
    public void sendUnOrderedBroadcast() {
        Intent intent = new Intent(MY_BROADCAST_ACTION_UNORDERED);//自定义广播动作。广播一般用于【应用程序之间】消息的传递,使用隐式意图。
        intent.putExtra("msg", "======发1万块======");
        intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);//包含已经停止的包。设置此Flag后,即使APP已退出,能匹配此Action的广播接收者依然可以接收到此广播
        //但是对于系统广播,由于Intent是系统内部直接发出的,无法更改此flag值,所以,3.1以后,如果App进程已经退出,广播接收者将不能接收到系统广播
        sendBroadcast(intent);
    }
    //发送有序广播。可被拦截,可终止,可以修改数据。
    public void sendOrderedBroadcast() {
        Intent intent = new Intent(MY_BROADCAST_ACTION_ORDERED);
        sendOrderedBroadcast(intent, null, null, null, Activity.RESULT_OK, "-----------给农民兄弟发10000块钱----------------", null);
        intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
        //String receiverPermission 只有指定权限的接受者才能接收到广播;BroadcastReceiver  一定会收到广播的接收者,不可以被拦截,但接收到的数据可被改变
        sendOrderedBroadcast(intent, null);//发送一条空广播
    }
    private class MyBRReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //广播中不允许开辟线程, onReceiver()运行超过10秒会ANR。广播更多的时候扮演的是一个"启动者"的角色,比如收到广播后启动Service,Notification,Activity等
            NetWorkEnum netEnum = NetUtils.getNetWorkState(MainActivity.this);
            Toast.makeText(context, "网络状态:" + netEnum, Toast.LENGTH_SHORT).show();
        }
    }
}

来电、去电、短信的BroadCastReceiver

/**监听来电、去电、短信到来*/
public class PhoneAndSmsReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) { //This method is called when the BroadcastReceiver is receiving an Intent broadcast。
        String resultData = getResultData(); //Retrieve the current result data, as set by the previous receiver. Often this is null.
        if (intent != null) {
            if (Intent.ACTION_NEW_OUTGOING_CALL.equalsIgnoreCase(intent.getAction())) {//去电。权限:CALL_PHONE
                doWhenTo(context, resultData);
            } else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equalsIgnoreCase(intent.getAction())) {//电话状态改变,和state一样,有三种状态
                if (TelephonyManager.EXTRA_STATE_RINGING.equalsIgnoreCase(intent.getStringExtra(TelephonyManager.EXTRA_STATE))) {//来电状态
                    doWhenFrom(context, resultData);
                }
            } else if (android.provider.Telephony.Sms.Intents.SMS_RECEIVED_ACTION.equalsIgnoreCase(intent.getAction())) {//沃日,这个字段隐藏的好深啊
                doWhenGetSms(context, intent);
            }
        }
    }
    /**当去电时,发送一个通知。这里可以直接获取到去电号码*/
    private void doWhenTo(Context context, String resultData) {
        Intent callIntent = new Intent(Intent.ACTION_CALL);
        callIntent.setData(Uri.parse("tel:" + resultData));//接听到的内容就是电话号码,如186********
        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, callIntent, 0);//注意,这里要用getActivity,因为PendingIntent的【目的】是打开拨号界面
        Notification notification = new Notification.Builder(context).setSmallIcon(R.drawable.ic_launcher)
                .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher))//
                .setTicker("TickerText:" + "监听到去电!").setAutoCancel(true).setContentTitle("包青天提醒你").setContentText("去电号码为" + resultData)//
                .setContentIntent(pendingIntent).setNumber(7).setWhen(System.currentTimeMillis()).build();//这个方法API 16 及之后才可使用
        ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(110, notification);//使用同一个id以替换旧的通知
    }
    /**当来电时,弹一个土司。注意,这里并不能获取到来电号码*/
    private void doWhenFrom(Context context, String resultData) {
        Toast.makeText(context, "监听到来电,resultData是否为null:" + (resultData == null), Toast.LENGTH_LONG).show();//这里获取到的resultData为null
    }
    /**当收到短信时,弹一个土司 */
    private void doWhenGetSms(Context context, Intent intent) {
        Object[] objs = (Object[]) intent.getExtras().get("pdus");
        for (Object obj : objs) {
            SmsMessage smsMessage = SmsMessage.createFromPdu((byte[]) obj);
            String sender = smsMessage.getOriginatingAddress();
            String body = smsMessage.getMessageBody();
            Toast.makeText(context, "收到消息-" + sender + "--" + body, Toast.LENGTH_LONG).show();
        }
        abortBroadcast();//根本干不过系统短信:①没启动时收不到此广播(这个很正常) ②启动时能收到但不能终止广播(估计优先级不够)
    }
}

电话状态监听PhoneStateListener

/**使用TelephonyManager监听电话状态。权限:READ_PHONE_STATE*/
public class MyPhoneStateListener extends PhoneStateListener {
    private Context context;
    public MyPhoneStateListener(Context context) {
        super();
        this.context = context;
    }
    public void onCallStateChanged(int state, String incomingNumber) {
        super.onCallStateChanged(state, incomingNumber);
        switch (state) {
        case TelephonyManager.CALL_STATE_IDLE:
            break;
        case TelephonyManager.CALL_STATE_OFFHOOK:
            break;
        case TelephonyManager.CALL_STATE_RINGING:
            Toast.makeText(context, "监听到来电,号码为:" + incomingNumber, Toast.LENGTH_LONG).show();
            break;
        }
    }
}

开机启动广播接收者

/** Android 4.3以上允许将应用安装在SD卡上,系统开机间隔一小段时间后才装载SD卡,为能获取开机广播,我们需要既监听开机广播又监听SD卡挂载广播 */
public class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "开机啦,赶快干坏事~", Toast.LENGTH_LONG).show();//基本上都会被手机屏蔽掉
        Intent mIntent = new Intent(context, MainActivity.class);
        mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//在广播中启动Activity需要添加FLAG_ACTIVITY_NEW_TASK,因为需要一个栈来存放Activity
        context.startActivity(mIntent);
    }
}

网络工具类

public class NetUtils {
    /**
     * 检查是否联网
     */
    public static boolean checkNetwork(Context context) {
        ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo network = manager.getActiveNetworkInfo();
        if (network == null) {
            return false;
        }
        return network.isAvailable();//或使用 return network.isConnectedOrConnecting();
    }
    /**
     * 判断当前网络连接是否为wifi
     */
    public static boolean isWifiConnection(Context context) {
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
        if (activeNetInfo != null && activeNetInfo.getType() == ConnectivityManager.TYPE_WIFI) {
            return true;
        }
        return false;
    }
    /**
     * 网络类型
     */
    public static enum NetWorkEnum {
        NETWORK_NONE, NETWORK_WIFI, NETWORK_2G, NETWORK_3G, NETWORK_4G
    }
    /**
     * 判断当前使用的网络状态
     * 返回值: 0: 没网络   1:WIFI   2:2G   3: 3G   4: 4G
     */
    public static NetWorkEnum getNetWorkState(Context context) {
        ConnectivityManager connectivityMag = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if (connectivityMag != null) {
            // 获取WIFI网络连接状态
            State wifiState = connectivityMag.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState();
            if (State.CONNECTED == wifiState || State.CONNECTING == wifiState) return NetWorkEnum.NETWORK_WIFI;
            // 检查是否有网络连接
            NetworkInfo netWorkInfo = connectivityMag.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
            if (netWorkInfo == null) return NetWorkEnum.NETWORK_NONE;
            else {
                State tempState = netWorkInfo.getState();
                if (State.CONNECTED == tempState || State.CONNECTING == tempState) return getNetworkClass(netWorkInfo.getSubtype());
                else return NetWorkEnum.NETWORK_NONE;
            }
        }
        return NetWorkEnum.NETWORK_NONE;
    }
    /**
     * 判断网络类型
     */
    private static NetWorkEnum getNetworkClass(int networkType) {
        switch (networkType) {
        case TelephonyManager.NETWORK_TYPE_GPRS:
        case TelephonyManager.NETWORK_TYPE_EDGE:
        case TelephonyManager.NETWORK_TYPE_CDMA:
        case TelephonyManager.NETWORK_TYPE_1xRTT:
        case TelephonyManager.NETWORK_TYPE_IDEN:
            return NetWorkEnum.NETWORK_2G;
        case TelephonyManager.NETWORK_TYPE_LTE:
            return NetWorkEnum.NETWORK_4G;
        case TelephonyManager.NETWORK_TYPE_UMTS:
        case TelephonyManager.NETWORK_TYPE_EVDO_0:
        case TelephonyManager.NETWORK_TYPE_EVDO_A:
        case TelephonyManager.NETWORK_TYPE_HSDPA:
        case TelephonyManager.NETWORK_TYPE_HSUPA:
        case TelephonyManager.NETWORK_TYPE_HSPA:
        case TelephonyManager.NETWORK_TYPE_EVDO_B:
        case TelephonyManager.NETWORK_TYPE_EHRPD:
        case TelephonyManager.NETWORK_TYPE_HSPAP:
        default:
            return NetWorkEnum.NETWORK_3G;
        }
    }
}

清单文件

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.bqt.broadcastreceiver"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="17"
        android:targetSdkVersion="21" />
    <!-- 获取网络信息状态 -->
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!-- 访问电话状态,使用TelephonyManager时需要 -->
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <!-- 监视、修改、放弃播出电话 -->
    <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <!-- —————————————————————-系统广播————————————————————— -->
        <!-- android:permission:如果设置,具有此权限的【广播发送者】发送的广播才能被此【广播接收者】所接收 -->
        <!-- android:process:指定运行时所处的进程。默认为APP的进程,四大组件都可以通过此属性指定自己的独立进程 -->
        <!-- android:exported:能否接收其他App(并非以进程为界)的发出的广播。同activity、service一样,如果有intent-filter默认值为true,否则为false -->
        <receiver android:name=".PhoneAndSmsReceiver" >
            <intent-filter>
                <!-- 静态注册的广播接收器即使app已经退出,只要有相应的广播发出,依然可以接收到,但此种描述自Android 3.1开始有可能不再成立 -->
                <action android:name="android.intent.action.NEW_OUTGOING_CALL" />
                <action android:name="android.intent.action.PHONE_STATE" />
                <action android:name="android.provider.Telephony.SMS_RECEIVED" />
            </intent-filter>
        </receiver>
        <!-- Android 4.3以上允许将应用安装在SD卡上,系统开机间隔一小段时间后才装载SD卡,所以我们需要既监听开机广播又监听SD卡挂载广播! -->
        <receiver android:name=".BootCompleteReceiver" >
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
            <intent-filter>
                <action android:name="ANDROID.INTENT.ACTION.MEDIA_MOUNTED" />
                <action android:name="ANDROID.INTENT.ACTION.MEDIA_UNMOUNTED" />
                <data android:scheme="file" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

附件列表

广播接收者 BroadcastReceiver 示例-1的更多相关文章

  1. 广播接收者 BroadcastReceiver 示例-2

    BaseActivity /**所有Activity的基类*/ public class BaseActivity extends Activity {     @Override     prote ...

  2. Android系统编程入门系列之广播接收者BroadcastReceiver实现进程间通信

    在前边几篇关于Android系统两个重要组件的介绍中,界面Activity负责应用程序与用户的交互,服务Service负责应用程序内部线程间的交互或两个应用程序进程之间的数据交互.看上去这两大组件就能 ...

  3. Android - 广播接收者 - BroadcastReceiver

    BroadcastReceiver 介绍: 广播是一种广泛运用的在应用程序之间传输信息的机制 .而 BroadcastReceiver 是对发送出来的广播 进行过滤接收并响应的一类组件 接受一种或者多 ...

  4. Android中广播接收者BroadcastReceiver详解

    1. 接收系统的广播步骤 (1)  新建一个类继承BroadcastReceiver 以监听sd卡状态的广播接收者为例 public class SdCardBroadcastReceiver ext ...

  5. Android学习笔记_19_广播接收者 BroadcastReceiver及其应用_窃听短信_拦截外拨电话

    一.广播接收者类型: 广播被分为两种不同的类型:“普通广播(Normal broadcasts)”和“有序广播(Ordered broadcasts)”. 普通广播是完全异步的,可以在同一时刻(逻辑上 ...

  6. 广播接收者BroadcastReceiver

    BroadcastReceiver与activity,service有完整的生命周期不同,BroadcastReceiver本质上是一系统级别的监听器,专门负责监听各程序发出的broadcast.与程 ...

  7. 广播接收者 BroadcastReceiver

    1. 分为动态注册和静态注册, 静态注册在清单文件里配置即可.动态创建为代码手动添加. 在锁屏广播中, 使用静态创建消息接受不成功, 原因未知. 动态即可. 代码如下: 2. 创建类, 继承与Broa ...

  8. 广播发送者&广播接收者介绍

    1.广播接收者 广播接收者简单地说就是接收广播意图的Java类,此Java类继承BroadcastReceiver类,重写: public void onReceive(Context context ...

  9. Android(java)学习笔记175:BroadcastReceiver之 外拨电话的广播接收者

    首先我们示例工程一览表如下: 1.首先我们还是买一个收音机,定义一个OutCallReceiver继承自BroadcastReceiver,onReceive()方法中定义了监听到广播,要执行的操作: ...

随机推荐

  1. iOS中解析 XML / JSON

    JSON数据格式 1. 概述: JSON (JavaScript Object Notation) 是⼀一种轻量级的数据交换格式 基于⽂文本格式,易于⼈人阅读和编写,同时也易于机器解析和⽣生成. 2. ...

  2. 【转】windows消息16进制对应表

    来源:http://blog.sina.com.cn/s/blog_962250db0101d4mj.html windows mobile编程,无论使用eVC还是.net CF,都脱不开window ...

  3. php中双$$与多$$

    <?php$a="b";$b="bbb";$c="ccc";echo $$a;?> 输出结果bbb $a的值为b $$a不是输出 ...

  4. startActivityForResult

    Activity提供了startActivityForResult(Intent intent, int requestCode)方法打开新的Activity,新的Activity关闭后会向前面的Ac ...

  5. Redis同步(主从复制)

    目录1.Replication的工作原理2.如何配置Redis主从复制3.应用示例 1.Replication的工作原理在Slave启动并连接到Master之后,它将主动发送一条SYNC命令.此后Ma ...

  6. tcpdump使用和TCP/IP包分析

    关于tcpdump如何抓包,本文不再总结,可以查看 tcpdump的官方地址查看http://www.tcpdump.org 本文重点记录两个部分:           第一部分:tcpdump所抓包 ...

  7. uva 10036 Problem C: Divisibility

    题意:能否在一个整数序列的每相邻的两项之间添加一个加减号,使得最终结果能被一个给定整数K<=100整除. dp[i][j]表示第i个数取余k为j的布尔值. #include <cstdio ...

  8. PCB Layout爬电距离、电气间隙的确定

    爬电距离的确定:首先需要确定绝缘的种类:基本绝缘:一次电路与保护地工作绝缘 ① :一次电路内部:二次电路内部工作绝缘 ② :输入部分(输入继电器之前)内部,二次电路与保护地加强绝缘:一次电路与二次电路 ...

  9. Windows 中默认安装的.Net 版本

    Windows contains a version of .NET by default. Here's a listing of them. XP .NET v1.0 -- Service pac ...

  10. Android写入文件操作权限

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses- ...