zoukankan      html  css  js  c++  java
  • Android WifiP2p实现

    Android WifiP2p实现

            Wifi Direct功能早在Android 4.0就以经加入Android系统了,但是一直没有很好的被支持,主要原因是比较耗电而且连接并不是很稳定。但是也有很大的好处,就是范围广而且速度快,适合设备间在无网络的情况下进行大文件传输。目前Android系统中只是内置了设备的搜索和链接功能,并没有像蓝牙那样有许多应用。
            有关WiFiP2P的相关API都在android.net.wifi.p2p下,类并不多,如下图:
            
            在做开发之前,我们首先简单了解一下WIFi P2P的模型。P2P模型中,是以一个组形式存在的,当两台设备通过P2P连接后,会随机(也可以手动指定)指派其中一台设备为组拥有者(Group Owner),相当于一台服务器,另一台设备为组成员(Group Client)。其他设备可以通过与GO设备连接加入组,但是不能直接和GC设备连接。在组内,成员可以直接获取到组长的IP地址,组长能直接获得组内成员的信息,但直接获取不到组员的IP地址,组员也不能直接获取到其他成员的信息。
            这里就相当于一个服务器--客户端模型。客户端能直接连接到服务器,服务器事先并不能连接到客户端,客户端本身也不知道其他客户端的存在,也不能直接建立联系。不过这些问题只是API没有提供对应方法而已,我们都可以通过软件手段进行解决。
            在做开发之前,我们先梳理一下逻辑。如果通过P2P传输文件,首先要建立连接,这里需要借助WifiP2pManager等类,建立连接后传输文件就用到Socket,这里就和P2P相关API无关了,属于Java的范畴。(有关TCPUDP的实现可以参考的我两篇笔记).


    一、建立连接
          第一步是建立连接,我们可以直接选择转到系统设置界面,代码如下:

    Intent p2pSettings = new Intent();
    p2pSettings.setComponent(new ComponentName("com.android.settings","com.android.settings.Settings$WifiP2pSettingsActivity"));
    try{
          startActivity(p2pSettings);
    }catch (Exception e){
          e.printStackTrace();
    }
    

          也可以自己实现搜索连接的逻辑,下面我们学习一下相关逻辑,可以参考Google开发文档或者Settings源码。
    1、首先我们要申请一些权限

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    

          若要传输文件,还要读写的权限,这组权限要动态申请

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

    2、WifiP2pManager的获取和初始化如下

    wifiP2pManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
    channel = wifiP2pManager.initialize(this,getMainLooper(),()->Utils.d("onChannelDisconnected"));
    

          其中channel是许多操作的真正执行者,而且许多操作都是异步的,需要借助很多接口实现。
    3、注册广播
          关于WiFi P2P的开发,大部分都借助于WifiP2pManager实现,而且一些状态的获取都是基于广播的,所以我们需要建立一个广播接受者,来接受各种相关的广播,首先需要注册下面几个广播:

    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION);
    registerReceiver(receiver, intentFilter);
    
    • WIFI_P2P_STATE_CHANGED_ACTION:P2P状态改变的广播,有两个状态:可用:WifiP2pManager.WIFI_P2P_STATE_ENABLED,不可用:WifiP2pManager.WIFI_P2P_STATE_DISABLED,在广播接受者内通过下面代码获取:
    case WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION:
          boolean p2pIsEnable = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE,WifiP2pManager.WIFI_P2P_STATE_DISABLED) == WifiP2pManager.WIFI_P2P_STATE_ENABLED;
          break
    
    • WIFI_P2P_PEERS_CHANGED_ACTION:当发现周围设备时的广播,一般在接到次广播时可以更新设备列表,与蓝牙不同,这里的API是以列表的形式将所有搜索到的设备都返回给我们的,具体如下:
    case WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION:
          //获取到设备列表信息
          WifiP2pDeviceList mPeers = intent.getParcelableExtra(WifiP2pManager.EXTRA_P2P_DEVICE_LIST);
          list.clear(); //清除旧的信息
          list.addAll(mPeers.getDeviceList()); //更新信息
          adapter.notifyDataSetChanged();  //更新列表
          break;
    

          这里很多教程中都用提出用wifiP2pManager.requestPeers()方法获取列表,但亲测这个方法并不好用,很多时候返回的列表为空,但实际上已搜索到设备了。有疑问的朋友可以两种方法都尝试一下,也可能我的手机framework层代码有变化。另外有些朋友可能觉得addAll方法比较消耗性能,其实这个广播不仅在周围设备增减时发送,而且在周围设备和本机设备的连接状态发送变化时,也会发出,这样可以及时更新设备状态。

    • WIFI_P2P_CONNECTION_CHANGED_ACTION:这是连接状态发送变化时的广播,如连接了一个设备,断开了一个设备都会接收到广播。着这个广播到来时,可以获得如下信息:
    case WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION:
          NetworkInfo networkInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
          WifiP2pInfo wifiP2pInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);
          WifiP2pGroup wifiP2pGroup = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);
          break;
    

          NetworkInfo 的isConnected()可以判断时连接还是断开时接收到的广播。
          WifiP2pInfo保存着一些连接的信息,如groupFormed字段保存是否有组建立,groupOwnerAddress字段保存GO设备的地址信息,isGroupOwner字段判断自己是否是GO设备。WifiP2pInfo也可以随时用过wifiP2pManager.requestConnectionInfo来获取。
          WifiP2pGroup存放着当前组成员的信息,这个信息只有GO设备可以获取。同样这个信息也可以通过wifiP2pManager.requestGroupInfo获取,一些方法如下,都比较简单易懂:
          

    • WIFI_P2P_THIS_DEVICE_CHANGED_ACTION:这个广播与当前设备的改变有关,一般注册这个广播后,就会收到,以此来获取当前设备的信息,具体如下:
    case WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION:
          WifiP2pDevice device = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE);
          deviceInfo.setText(getString(R.string.device_info) + device.deviceName );
          break;
    
    • WIFI_P2P_DISCOVERY_CHANGED_ACTION:这个是搜索状态有关的广播,开始搜索和结束搜索时会收到,用法如下:
    case WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION:
             int discoveryState = intent.getIntExtra(WifiP2pManager.EXTRA_DISCOVERY_STATE,WifiP2pManager.WIFI_P2P_DISCOVERY_STOPPED);
             isDiscover = discoveryState == WifiP2pManager.WIFI_P2P_DISCOVERY_STARTED;
             updateState();
             break;
    

          通过广播我们可以及时获取到各种状态的改变。

    4、启动搜索
          启动搜索的方法如下:

    wifiP2pManager.discoverPeers(channel, new WifiP2pManager.ActionListener() {
                @Override
                public void onSuccess() {
                    Utils.d("discoverPeers Success");
                }
    
                @Override
                public void onFailure(int reason) {
                    Utils.d("discoverPeers Failure");
                }
            });
    

    5、停止搜索
          停止搜索的方法如下:

    wifiP2pManager.stopPeerDiscovery(channel, null);
    

          一般在退出时停止搜索,也就不需要进行状态监听。需要注意的是,在开启搜索时设备才是可见,停止时设备就变得不可见了。
    6、连接设备
          连接设备操作如下:

    WifiP2pDevice selectDevice = list.get(position);
    if (selectDevice.status == WifiP2pDevice.AVAILABLE) {
        WifiP2pConfig config = new WifiP2pConfig();
        config.deviceAddress = selectDevice.deviceAddress;
        config.wps.setup = WpsInfo.PBC;
        wifiP2pManager.connect(channel, config, new WifiP2pManager.ActionListener() {
               @Override
                public void onSuccess() {
                       Utils.d("connect success");
                }
    
               @Override
               public void onFailure(int reason) {
                       Utils.d("connect failure");
               }
        });
    }
    

          基本就是选择一个要连接的设备,配置一个WifiP2pConfig对象,调用connect方法进行连接,由于p2p相关API大多都是异步的,需要一个监听成功与否的操作。需要注意的是,设备有很多状态,只有处于Available状态时才可以尝试连接,另外还有Invited和Connected状态等。Connected就是已经连接,Invited是指请求过连接,对方可能处于另外一个组中并且是GC身份,不能与其他设备连接,请求被搁置。

          当处于Invited时可以取消邀请:

    WifiP2pDevice selectDevice = list.get(position);
    if (selectDevice.status == WifiP2pDevice.INVITED){
          wifiP2pManager.cancelConnect(channel, new WifiP2pManager.ActionListener() {
                  public void onSuccess() {
                         Utils.d(" cancel connect success");
                  }
                  public void onFailure(int reason) {
                         Utils.d(" cancel connect fail ");
                   }
          });
    }
    

          当处于Connected状态时可以断开连接:

    WifiP2pDevice selectDevice = list.get(position);
    if (selectDevice.status == WifiP2pDevice.CONNECTED) {
             wifiP2pManager.removeGroup(channel, new WifiP2pManager.ActionListener() {
                    public void onSuccess() {
                           Utils.d(" remove group success");
                    }
                    public void onFailure(int reason) {
                           Utils.d(" remove group fail " );
                    }
             });
    }
    

          我们之前说,建立连接时,身份是随机分配的,不过我们可以指定自己作为GO设备,通过wifiP2pManager.createGroup创建组,来等待客户端连接。
          在连接成功后,利用WifiP2pInfo的groupOwnerAddress获取到GO设备也就是服务器的地址,进行通信。
          可以看到以上的操作进行完之后,若是利用socket等基于IP地址的通信方式,只能GC设备主动和GO设备通信后,GO设备才能和GC设备通信。
          我的做法是服务端维护一个表,通过mac地址区分设备,当一个连接建立时,客户端主动和服务端进行一次极短的通信,服务端借此保存客户端的地址和端口,当连接中断后或者新设备加入时更新这个表。


    二、设备通信
          建立连接后,可以利用TCP协议进行通信,在设计模型时,我的做法是,在P2P服务可用时,就启动一个服务,建立一个ServerSocket,并设立一个死循环,不断接受外部的请求,由于在主线程中不能这样做,可以借助IntentService,实现如下,以接受文件为例:

    public class WifiP2PReceiveService extends IntentService {
    
        public WifiP2PReceiveService() {
            super("WifiP2PReceiveService");
        }
    
        @Override
        protected void onHandleIntent(Intent intent) {
            Utils.d("receive start");
            try (ServerSocket service = new ServerSocket(10101)){
                while (true){
                    Utils.d("wait accept");
                    try (Socket socket = service.accept()){
                        Utils.d("get accept");
                        InputStream in = socket.getInputStream();
                        ObjectInputStream objectInputStream = new ObjectInputStream(in);
                        //获取文件信息
                        FileInfo info = (FileInfo) objectInputStream.readObject();
                        //创建存储目录
                        File downdir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),"wifip2p");
                        if(!downdir.exists())
                            downdir.mkdir();
                        File file = new File(downdir,info.getName());
                        //存储文件
                        FileOutputStream fileOutputStream = new FileOutputStream(file);
                        int len;
                        byte[] buffer = new byte[1024*8];
                        long total = 0;
                        while((len = in.read(buffer))!=-1){
                            fileOutputStream.write(buffer,0,len);
                            total += len;
                            Utils.d("current : " + total + " / total : " + info.getLength());
                        }
                        //MD5校验
                        Utils.d(info.getMd5().equals(Utils.getMD5(file)) ? "传输成功" : "传输失败,MD5不一致");
                        Utils.d("info:"+file.toString());
                    }catch (Exception e){
                        Utils.d(e.getMessage());
                    }
                }
            }catch (Exception e){
                Utils.d(e.getMessage());
            }
        }
    
    }
    

          当然上面也可以利用线程池进行设计,减少阻塞,具体见我的TCP相关笔记。发送文件也比较简单,发送时不必让服务一直存在,还是可以借助IntentService,用完自动回收,而且在子线程操作:

    public class WifiP2PSendService extends IntentService {
        private InetAddress address = null;
        private FileInfo fileinfo;
    
        public WifiP2PSendService() {
            super("WifiP2PSendService");
    
        }
        
        @Override
        protected void onHandleIntent(Intent intent) {
            //传入文件信息
            fileinfo = (FileInfo) intent.getSerializableExtra("fileinfo");
            //传入地址信息
            address = (InetAddress) intent.getSerializableExtra("address");
            try (Socket socket = new Socket(address,10101)){
                OutputStream out = socket.getOutputStream();
                //开始传输
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);
                objectOutputStream.writeObject(fileinfo);
                FileInputStream fileInputStream = new FileInputStream(new File(fileinfo.getPath()));
                int len = 0;
                byte[] buffer = new byte[1024];
                long total = 0;
                while((len = fileInputStream.read(buffer))!=-1) {
                    out.write(buffer, 0, len);
                    total += len;
                    Utils.d("current : " + total + " / total : " + fileinfo.getLength());
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    
    }
    

          记得在退出时结束WifiP2PReceiveService。


          最后提供一下文件选择时的操作代码:

    public void send(View view) {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("*/*");
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        startActivityForResult(intent, 1);
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode == 1){
            if (resultCode == RESULT_OK){
                Uri uri = data.getData();
                if (uri != null) {
                    String path = Utils.getPath(this,uri);
                    if (path != null) {
                        final File file = new File(path);
                        if (!file.exists() ) {
                            d("找不到文件");
                            return;
                        }
                        //计算MD5值时建议放在发送文件的服务中,也就是子进程中完成
                        String md5 = Utils.getMD5(file);
                        FileInfo fileinfo = new FileInfo(file.getName(), file.length(), md5,file.getAbsolutePath());
                        Intent intent = new Intent();
                        intent.putExtra("address",connectDevice.groupOwnerAddress);
                        intent.putExtra("fileinfo",fileinfo);
                        intent.setClass(this,WifiP2PSendService.class);
                        startService(intent);
                    }
                }
            }
        }
    }
    

          通过URI解析文件路径:

    public static String getPath(Context context, Uri uri){
        if (context == null || uri == null)
            return null;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && DocumentsContract.isDocumentUri(context, uri)) {
            if (isExternalStorageDocument(uri)) {
                String docId = DocumentsContract.getDocumentId(uri);
                String[] split = docId.split(":");
                String type = split[0];
                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }
            } else if (isDownloadsDocument(uri)) {
                String id = DocumentsContract.getDocumentId(uri);
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
                return getDataColumn(context, contentUri, null, null);
            } else if (isMediaDocument(uri)) {
                String docId = DocumentsContract.getDocumentId(uri);
                String[] split = docId.split(":");
                String type = split[0];
                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }
                String selection = MediaStore.Images.Media._ID + "=?";
                String[] selectionArgs = new String[]{split[1]};
                return getDataColumn(context, contentUri, selection, selectionArgs);
            }
        }
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            if (isGooglePhotosUri(uri))
                return uri.getLastPathSegment();
            return getDataColumn(context, uri, null, null);
        }
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }
        return null;
    }
    
    public static boolean isExternalStorageDocument(Uri uri) {
        return "com.android.externalstorage.documents".equals(uri.getAuthority());
    }
    
    
    public static boolean isDownloadsDocument(Uri uri) {
        return "com.android.providers.downloads.documents".equals(uri.getAuthority());
    }
    
    
    public static boolean isMediaDocument(Uri uri) {
        return "com.android.providers.media.documents".equals(uri.getAuthority());
    }
    
    
    public static boolean isGooglePhotosUri(Uri uri) {
        return "com.google.android.apps.photos.content".equals(uri.getAuthority());
    }
    
    public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
        Cursor cursor = null;
        String column = MediaStore.Images.Media.DATA;
        String[] projection = {column};
        try {
            cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
            if (cursor != null && cursor.moveToFirst()) {
                int index = cursor.getColumnIndexOrThrow(column);
                return cursor.getString(index);
            }
        } finally {
            if (cursor != null)
                cursor.close();
        }
        return null;
    }
    

          计算MD5值:

    public static String getMD5(File file){
        InputStream inputStream = null;
        byte[] buffer = new byte[2048];
        int numRead;
        MessageDigest md5;
        try {
            inputStream = new FileInputStream(file);
            md5 = MessageDigest.getInstance("MD5");
            while ((numRead = inputStream.read(buffer)) > 0) {
                md5.update(buffer, 0, numRead);
            }
            StringBuilder hexValue = new StringBuilder();
            for (byte b : md5.digest()) {
                int val = ((int) b) & 0xff;
                if (val < 16) {
                    hexValue.append("0");
                }
                hexValue.append(Integer.toHexString(val));
            }
            return hexValue.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    转自[1]Android WiFi P2P开发实践笔记
    参考[1]Wifi P2p点对点连接详解
         [2]p2p通信原理及实现

  • 相关阅读:
    CocoaPod 常用命令
    Runloop
    RxSwift学习笔记7:buffer/window/map/flatMap/flatMapLatest/flatMapFirst/concatMap/scan/groupBy
    RxSwift学习笔记6:Subjects/PublishSubject/BehaviorSubject/ReplaySubject/Variable
    RxSwift学习笔记5:Binder
    RxSwift学习笔记4:disposeBag/scheduler/AnyObserver/Binder
    RxSwift学习笔记3:生命周期/订阅
    RxSwift学习笔记2:Observable/生命周期/Event/oneNext/onError/onCompleted/
    RxSwift学习笔记1:RxSwift的编程风格
    iOS处理视图上同时添加单击与双击手势的冲突问题
  • 原文地址:https://www.cnblogs.com/shujk/p/15028543.html
Copyright © 2011-2022 走看看