【Android编程】通过Intent.ACTION_VIEW来实现Android8.0及以上的Wifi热点开关

1,410 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第33天,点击查看活动详情.

项目效果图展示:

Server Client

开发内容大致分为以下几点:

  1. 在 AndroidManifest 中声明相关权限
  2. Server端可以手动开启和关闭Ap热点
  3. Server端可以获取到连接至本机热点的所有Client,并能够在页面中实时更新Client列表信息
  4. Client端可以自动打开Wifi,并循环连接固定的Ap热点,直至连接成功
  5. Client端可以显示手机型号、成功连接至Ap后所分配的ip地址以及连接状态。

一、权限声明

本应用并不会消耗移动数据,但由于要使用到Wifi,所以需要申请网络相关的权限,而随着Android系统的不断升级,导致Android8.0以后,获取wifi的ssid的方法有所变动,没有办法直接获取ssid,9.0以上需要申请定位权限,10.0需要申请新添加的隐私权限ACCESS_FINE_LOCATION。

  • AndroidManifest.xml
 <uses-permission android:name="android.permission.TETHER_PRIVILEGED"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />

动态申请权限

  • BaseActivity.java
 private void settingPermission() {
        mSettingPermission = true;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.System.canWrite(getApplicationContext())) {
                mSettingPermission = false;
                Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS, Uri.parse("package:" + getPackageName()));
                startActivityForResult(intent, MY_PERMISSIONS_MANAGE_WRITE_SETTINGS);
            }
        }
    }
    
    private void locationsPermission(){
        mLocationPermission = true;
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            mLocationPermission = false;
            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION)) {
            } else {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                        MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // Check which request we're responding to
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == MY_PERMISSIONS_MANAGE_WRITE_SETTINGS) {
            // Make sure the request was successful
            if (resultCode == RESULT_OK) {
                mSettingPermission = true;
                if (!mLocationPermission) locationsPermission();
            } else {
                settingPermission();
            }
        }
        if (requestCode == MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
            // Make sure the request was successful
            if (resultCode == RESULT_OK) {
                mLocationPermission = true;
                if (!mSettingPermission) settingPermission();
            } else {
                locationsPermission();
            }
        }
        if (mLocationPermission && mSettingPermission) onPermissionsOkay();
    }

二、Server端

Server端计划是能够自动开启Ap热点供Client端连接,传统方式是通过反射来开启热点,但由于其并不适用于Android 8.0(Oreo)及以上版本。后经一番搜索查询,最终通过Jon Robinson提供的WifiHotSpot示例程序成功实现了在Android 8.0以上版本手机以编程的方式自动开启或关闭Ap热点,但其最终实现效果仍存在一些问题,如:快速切换Server/Client身份时,可能有时候会导致Ap热点未能成功自动开启,故为了弥补这个缺陷,又在Server端中加入触发按钮,使其可以在未能成功自动开启Ap热点时,也能通过一种主动的方式来开启或关闭Ap热点。

Server端通过sendImplicitBroadcast()方法来发送隐式广播意图给广播接收器

  • MainActivity.java
 /** 开启热点 **/
    public void onClickTurnOnAction(View v){
        Intent intent = new Intent(getString(R.string.intent_action_turnon));
        sendImplicitBroadcast(this,intent);
    }
    /** 关闭热点 **/
    public void onClickTurnOffAction(View v){
        Intent intent = new Intent(getString(R.string.intent_action_turnoff));
        sendImplicitBroadcast(this,intent);
    }

    /** 发送广播 **/
    private static void sendImplicitBroadcast(Context ctxt, Intent i) {

        // 获得一个PackageManger对象
        PackageManager pm=ctxt.getPackageManager();
        // 检索所有可以处理给定Intent广播的接收器。
        List<ResolveInfo> matches=pm.queryBroadcastReceivers(i, 0);
        // 遍历matches
        for (ResolveInfo resolveInfo : matches) {
            Intent explicit=new Intent(i);
             // ComponentName:可以启动其他应用的Activity、Service.
            ComponentName cn= new ComponentName(resolveInfo.activityInfo.applicationInfo.packageName,
                    resolveInfo.activityInfo.name);
            explicit.setComponent(cn);
            ctxt.sendBroadcast(explicit);
        }
    }

通过在AndroidManifest.xml中注册广播接收器,用注册的HotSpotIntentReceiver来监听自己制定的广播意图

  • AndroidManifest.xml
 <receiver android:name=".receiver.HotSpotIntentReceiver" android:enabled="true" android:exported="true">
            <intent-filter>
                <action android:name="com.sdust.lazytom.apconnect.TURN_ON"/>
                <action android:name="com.sdust.lazytom.apconnect.TURN_OFF"/>
            </intent-filter>
        </receiver>
  • HotSpotIntentReceiver.java

一旦拦截到com.sdust.lazytom.apconnect.TURN_ON和com.sdust.lazytom.apconnect.TURN_OFF这两个广播意图,onReceive()中实现的逻辑代码就开始执行。

 @Override
    public void onReceive(Context context, Intent intent) {
        final String ACTION_TURNON = context.getString(R.string.intent_action_turnon);
        final String ACTION_TURNOFF = context.getString(R.string.intent_action_turnoff);
        Log.i(TAG,"Received intent");
        if (intent != null) {
            final String action = intent.getAction();
            if (ACTION_TURNON.equals(action)) {
                MagicActivity.useMagicActivityToTurnOn(context);
            } else if (ACTION_TURNOFF.equals(action)) {
                MagicActivity.useMagicActivityToTurnOff(context);
            }
        }
    }

Intent.ACTION_VIEW:用于显示用户的数据。比较通用,会根据用户的数据类型打开相应的Activity,比如 tel:134000101打开拨号程序,www.google.com则会打开浏览器等,此处为开/关Ap热点。

  • MagicActivity.java
 public static void useMagicActivityToTurnOn(Context c){
        Uri uri = new Uri.Builder().scheme(c.getString(R.string.intent_data_scheme)).authority(c.getString(R.string.intent_data_host_turnon)).build();
        Toast.makeText(c,"热点开启!",Toast.LENGTH_LONG).show();
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setData(uri);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        c.startActivity(i);
    }

    public static void useMagicActivityToTurnOff(Context c){
        Uri uri = new Uri.Builder().scheme(c.getString(R.string.intent_data_scheme)).authority(c.getString(R.string.intent_data_host_turnoff)).build();
        Toast.makeText(c,"热点关闭!",Toast.LENGTH_LONG).show();
        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setData(uri);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        c.startActivity(i);
    }

此时Server端已经成功实现了通过编程的方式主动开启或关闭Ap热点,下面就是如何将连接至本机Ap热点的Client全部显示在页面上。显示页面采用了普通的LinearLayout布局,内嵌套RecyclerView来实现Client端列表的展示

首先在activity_main中引入RecyclerView

  • activity_main.xml
  <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_deviceList"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

然后创建Phone实体类

  • Phone.java
public class Phone {

    private String ip;
    private String device;
    private String status;
    private int imageId;
    
    public String getIp(){return ip;}
    public int getImageId() {return imageId; }
    public String getStatus(){return  status;}
    public String getDevice() {return device; }

}

新建phone_item来显示单个Client

  • phone_item.xml

在这里插入图片描述 新增适配器PhoneAdapter,并让其继承于RecyclerView.Adapter,把泛型指定为PhoneAdapter.ViewHolder

  • PhoneAdapter.java
public class PhoneAdapter extends RecyclerView.Adapter<PhoneAdapter.ViewHolder> {

    private List<Phone> mPhoneList;

    static class ViewHolder extends RecyclerView.ViewHolder {
        ImageView phoneimage;
        TextView phoneip;
        TextView phonestatus;
        TextView phonedevice;

        public ViewHolder(View view) {
            super(view);
            phoneimage = (ImageView) view.findViewById(R.id.phone_image);
            phoneip = (TextView) view.findViewById(R.id.phone_ip);
            phonestatus = (TextView) view.findViewById(R.id.phone_status);
            phonedevice = (TextView) view.findViewById(R.id.phone_device);
        }
    }

    public PhoneAdapter(List<Phone> phoneList) {
        mPhoneList = phoneList;
    }

    @Override

    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.phone_item, parent, false);
        ViewHolder holder = new ViewHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Phone phone = mPhoneList.get(position);
        holder.phoneimage.setImageResource(phone.getImageId());
        holder.phoneip.setText(phone.getIp());
        holder.phonestatus.setText(phone.getStatus());
        holder.phonedevice.setText(phone.getDevice());
    }

    @Override
    public int getItemCount() {
        return mPhoneList.size();
    }
}

修改MainActivity,使其显示获取到的Client列表

  • MainActivity.java
 /** 显示RecyclerView **/
    public void refresh(){
        phoneList.removeAll(phoneList);
        ArrayList<Phone> connectedIP = null;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            connectedIP = getConnectedIP();
        }

        for (Phone phone : connectedIP) {
            if (!phone.getIp().equals("IP")) {
                Phone device = new Phone(phone.getIp(),phone.getStatus(), phone.getDevice(),R.drawable.android_device);
                phoneList.add(device);
            }
        }
        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rv_deviceList);
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        PhoneAdapter adapter = new PhoneAdapter(phoneList);
        recyclerView.setAdapter(adapter);
        System.out.println("ad count:"+adapter.getItemCount());
    }

最后通过定时器实时更新RecyclerView列表

 /** 1S更新一次RecyclerView **/
        Handler handler=new Handler();
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                refresh();
                handler.postDelayed(this, 2000);
            }
        };
        handler.postDelayed(runnable, 1000);//每1秒执行一次runnable.

三、Client端

作为Client端,首先要能够主动连接Server端开启的Wifi热点

  • WifiLManager.java
 /**
     * 连接指定Wifi
     *
     * @param context  上下文
     * @param ssid     SSID
     * @param password 密码
     * @return 是否连接成功
     */
    public static int connectWifi(Context context, String ssid, String password) {



        String connectedSsid = getConnectedSSID(context);
        if (!TextUtils.isEmpty(connectedSsid) && connectedSsid.equals(ssid)) {
            return 3;
        }
        // ApManager.closeAp(context);
        WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        if (wifiManager == null) {
            return 0;
        }
        if (!wifiManager.isWifiEnabled()) {
            wifiManager.setWifiEnabled(true);
        }
        WifiConfiguration wifiConfiguration = isWifiExist(context, ssid);
        if (wifiConfiguration == null) {
            wifiConfiguration = createWifiConfiguration(ssid, password);
        }
        int networkId = wifiManager.addNetwork(wifiConfiguration);
        wifiManager.enableNetwork(networkId, true);
        int connectState = wifiManager.getWifiState();
        return connectState ;
    }

创建NetworkConnectChangedReceiver广播接收器来监听wifi连接状态的变化,如果连接中断或失败,则进行重新连接,同时通过其监听到的连接状态的变化来动态显示分配到的ip及连接状态

  • ClientActivity.java
// 注册广播
 IntentFilter filter = new IntentFilter();
        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        networkConnectChangedReceiver = new NetworkConnectChangedReceiver();
        registerReceiver(networkConnectChangedReceiver, filter);

 /** 接收Wifi连接状态变化 **/
    class NetworkConnectChangedReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction() == WifiManager.NETWORK_STATE_CHANGED_ACTION) {
                NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
                if (networkInfo != null) {
                    boolean isWifiConnected = networkInfo.isConnected();
                    if(isWifiConnected){
                        Log.i("state","WIFI is connected");
                        tv_myDeviceStatus.setText("已连接");
                        tv_myDeviceAddress.setText(getLocalIpAddress(context));
                    } else {
                        Log.i("state","WIFI is disconnected");
                        tv_myDeviceStatus.setText("未连接");
                        tv_myDeviceAddress.setText(getLocalIpAddress(context));
                        connectWifi(context,AP_SSID,AP_PASSWORD);
                    }
                }
            }

        }
    }

四、Server和Client的切换

通过安卓轻量级选择按钮——SwitchButton,来切换不同的页面,以此实现Server和Client身份的转变

  • MainActivity.java
private SwitchButton sb_job;
sb_job = findViewById(R.id.sb_job);
        sb_job.setChecked(false);
        sb_job.isChecked();
        sb_job.toggle();     //switch state
        sb_job.toggle(false);//switch without animation
        sb_job.setShadowEffect(true);//disable shadow effect
        sb_job.setEnabled(true);//disable button
        sb_job.setEnableEffect(true);//disable the switch animation
        sb_job.setOnCheckedChangeListener(new SwitchButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(SwitchButton view, boolean isChecked) {
                //TODO do your job
                //导航
                Intent intent = new Intent();
                intent.setClass(MainActivity.this, ClientActivity.class);
                startActivity(intent);
                Intent apintent = new Intent(getString(R.string.intent_action_turnoff));
                sendImplicitBroadcast(MainActivity.this,apintent);
                openWifi(MainActivity.this);
                finish();
            }
        });