背景
最近要做一个demo,目的是实现局域网内的两台手机之间的文件互传。具体流程如下:
- 手机 A 从服务器上下载一个 apk 文件到本机上;
- 手机 A 在自己的某个端口上启动一个 Server 服务,供手机 B 对刚刚的 apk 进行下载;
- 手机 B 请求手机 A 的 ip 地址与端口,进行 apk 的下载;
实现过程
目前只是一个最简版的实现。
1. 首先因为是下载服务,就在 app 里面动态申请了读写权限;
//读写权限
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
// 请求状态码
private static int REQUEST_PERMISSION_CODE = 1;
// 动态申请读写权限
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_PERMISSION_CODE);
}
}
2. 同样因为是下载服务,于是用了一个网络监听器对网络情况进行监听,但是还没有实现网络情况随时更改的监听;
public class NetworkStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
System.out.println("网路状态发生变化");
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo wifiNetworkInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
NetworkInfo dataNetworkInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
if (wifiNetworkInfo.isConnected() && dataNetworkInfo.isConnected())
{
Toast.makeText(context, "Wifi 已连接,移动数据已连接", Toast.LENGTH_SHORT).show();
} else if (wifiNetworkInfo.isConnected() && !dataNetworkInfo.isConnected()){
Toast.makeText(context, "Wifi 已连接,移动数据尚未连接", Toast.LENGTH_SHORT).show();
} else if (!wifiNetworkInfo.isConnected() && dataNetworkInfo.isConnected()){
Toast.makeText(context, "Wifi 尚未连接,移动数据已连接", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "Wifi 尚未连接,移动数据尚未连接", Toast.LENGTH_SHORT).show();
}
}
else {
System.out.println("API level higher than 23");
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Network[] networks = manager.getAllNetworks();
StringBuilder sb = new StringBuilder();
Boolean result = false;
if (networks.length > 0) {
for (int i = 0; i < networks.length; i++) {
NetworkInfo networkInfo = manager.getNetworkInfo(networks[i]);
result |= (networkInfo.getType() == ConnectivityManager.TYPE_WIFI);
sb.append(networkInfo.getTypeName() + " connect is " + networkInfo);
}
if (result) {
Toast.makeText(context, "Wifi 已连接", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "Wifi 尚未连接", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(context, "Wifi 尚未连接,移动数据尚未连接", Toast.LENGTH_SHORT).show();
}
}
}
}
3. 手机 A 在这里主要设计了两个按钮,一个是从服务器端(此时手机 A 作为 client)的下载按钮,另外一个是提供给手机 B 的下载按钮(此时手机 A 作为 server)。
private void initView()
{
btDownload = findViewById(R.id.btn_downloadServer);
btUpload = findViewById(R.id.btn_upload);
tv = findViewById(R.id.textView);
// client_content = findViewById(R.id.client_content);
// ip = findViewById(R.id.ip);
//
// serverIp = IPUtils.getlocalip(MainActivity.this);
// ip.setText("IP Address: " + serverIp);
btUpload.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
HttpClientUtil httpClientUtil = new HttpClientUtil();
httpClientUtil.startServer();
}
});
btDownload.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{
String url = "https://github.com/amirzaidi/Launcher3/releases/download/Pixel-v2.1/Launcher3-aosp-debug.apk";
String fPath = Environment.getExternalStorageDirectory() + File.separator + "Download";
String fName = "Launcher3-aosp-debug.apk";
if (NetworkUtils.isNetworkAvailable(MainActivity.this)){
if (!isFileExists(fPath, fName)){
new DownloadTask().execute(url, fPath, fName);
}
else
{
Toast.makeText(MainActivity.this, "文件已存在!", Toast.LENGTH_SHORT).show();
}
}
else{
Toast.makeText(MainActivity.this, "未接入网络!", Toast.LENGTH_SHORT).show();
}
}
});
}
4. 由于 android 的主线程只能用于更新 UI,所以这里的下载采用了 AsyncTask 的方法,并用一个 textView 来进行下载状态的更新。
private class DownloadTask extends AsyncTask<String, Integer, String> {
@Override
protected void onPreExecute() {
super.onPreExecute();
tv.setText("开始下载");
}
@Override
protected String doInBackground(String... params) {
HttpConnectionUtil.downloadFile(params[0], params[1], params[2]);
return "从服务器下载完成!";
}
@Override
protected void onPostExecute(String result) {
tv.setText(result);
}
5. 下载部分的代码使用的是 HttpURLConnection。
public class HttpConnectionUtil {
public static Boolean downloadFile(String urlPath, String downloadDir, String fileName) {
InputStream is;
BufferedInputStream bis;
URL url;
HttpURLConnection httpURLConnection;
File file;
OutputStream fos;
int code;
try {
url = new URL(urlPath);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestMethod("GET");
httpURLConnection.setRequestProperty("Charset", "UTF-8");
httpURLConnection.connect();
code = httpURLConnection.getResponseCode();
Log.e("Http Connection", code + "");
if (code == HttpURLConnection.HTTP_OK){
int fileLength = httpURLConnection.getContentLength();
is = httpURLConnection.getInputStream();
bis = new BufferedInputStream(is);
String path = downloadDir + File.separatorChar + fileName;
file = new File(path);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
fos = new FileOutputStream(file);
int size;
int len = 0;
byte[] buf = new byte[1024];
while ((size = bis.read(buf)) != -1) {
len += size;
fos.write(buf, 0, size);
}
bis.close();
fos.close();
return len == fileLength;
}
else{
return false;
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
6. 手机 A 作为 server 部分的代码则使用了开源库,此处待修改的是 server.get 的 regex 太过简单了,还有就是需要检测网络变化。
public class HttpClientUtil {
private static String TAG = "HttpClientUtil";
private static final HttpClientUtil mInstance = new HttpClientUtil();
private AsyncHttpServer server = new AsyncHttpServer();
private int port = 5000;
private boolean isRunning = false;
private String ipAddress = "";
private String fPath = Environment.getExternalStorageDirectory() + File.separator + "Download";
private String fName = "Launcher3-aosp-debug.apk";
private boolean isNetworkConnect;
public static HttpClientUtil getInstance() {
return mInstance;
}
// private HttpClientUtil(Context context) {
//
// isNetworkConnect = NetworkUtils.isNetworkAvailable(context); // 目前尚未实现网络状态实时监测
//
// if (isNetworkConnect) {
// if (!isRunning) {
// startServer();
// }
// } else {
// if (isRunning) {
// stopServer();
// }
// }
// }
public void startServer() {
List<WebSocket> _sockets = new ArrayList<WebSocket>();
server.get("/", new HttpServerRequestCallback() {
@Override
public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
File file = new File(fPath + File.separator + fName);
response.sendFile(file);
}
});
server.listen(port);
}
public void stopServer() {
if (isRunning) {
return;
}
isRunning = false;
ipAddress = "";
server.stop();
Log.d(TAG, "[stopServer] Server stopped!");
}
}
7. 其他的一些小函数,isNetworkAvailable 与 getLocalIp。
public class NetworkUtils {
public static boolean isNetworkAvailable(Context context){
ConnectivityManager manager = (ConnectivityManager) context.getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (manager == null){
return false;
}
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnected()){
return false;
}
return true;
}
}
public class IPUtils {
public static String getLocalIp(Context context) {
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
int ipAddress = wifiInfo.getIpAddress();
// Log.d(Tag, "int ip "+ipAddress);
if (ipAddress == 0) return null;
return ((ipAddress & 0xff) + "." + (ipAddress >> 8 & 0xff) + "."
+ (ipAddress >> 16 & 0xff) + "." + (ipAddress >> 24 & 0xff));
}
}
实现效果
1. 手机 A 未开始下载时;
2 手机 A 从服务器下载完毕;
3. 可以看到名为 Launcher 的 apk 文件已经下载到了对应的目录下面了;
4. 手机 B 未进行下载前的状态;
5. 手机 B 从手机 A 开放的端口号开始进行 apk 的下载;
6. 手机 B 从手机 A 开放的端口号下载 apk 完毕的状态;
7. 验证一下手机 B 上的 apk 下载是否真的成功了,可以看到名为 Launcher 的 apk;