开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第33天,点击查看活动详情.
项目效果图展示:
开发内容大致分为以下几点:
- 在 AndroidManifest 中声明相关权限
- Server端可以手动开启和关闭Ap热点
- Server端可以获取到连接至本机热点的所有Client,并能够在页面中实时更新Client列表信息
- Client端可以自动打开Wifi,并循环连接固定的Ap热点,直至连接成功
- 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();
}
});