安卓APP实现在线更新功能,适配Androidx和9.0,以及Tomcat服务器的配置
1.需要用到的技术:
(1)Android
(2)Tomcat
(3)HTTP
2.效果展示:
3.实现步骤:
(1)Android项目的编写:
UpdateActivity.java活动
里面的端口号需要在Tomcat里的server.xml加以配置,后面有写。
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.example.administrator.loginclient.R;
import com.example.administrator.loginclient.Updates.LoadingService;
import com.example.administrator.loginclient.Updates.Utils;
import com.example.administrator.loginclient.Updates.VersionInfoBean;
import java.io.File;
public class UpdateActivity extends BaseActivity {
private Button btnRefresh;
private VersionInfoBean versionInfoBean;
private boolean isLoading;
private MyReceive myReceive;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_update);
btnRefresh = (Button)findViewById(R.id.btn_refresh);
myReceive = new MyReceive();
IntentFilter filter = new IntentFilter();
filter.addAction("android.intent.action.loading_over");
filter.addAction("android.intent.action.loading");
btnRefresh.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(isLoading){
Toast.makeText(UpdateActivity.this,"正在下载...",Toast.LENGTH_SHORT).show();
}else{
getVersionInfoFromServer();
}
}
});
}
@Override
protected void onPause(){
super.onPause();
myReceive = new MyReceive();
IntentFilter filter = new IntentFilter();
registerReceiver(myReceive,filter);
}
/**
* 从服务器获取版本最新的版本信息
*
*/
@SuppressLint("ApplySharedPref")
private void getVersionInfoFromServer(){
versionInfoBean = new VersionInfoBean("1.1.1","http://localhost:8083/PersonalHomePage/app-debug.apk","1.更新了xxxxx功能"
,getExternalCacheDir()+"/apk");
Log.v("下载成功...", getExternalCacheDir()+"/apk");
SharedPreferences sharedPreferences = getSharedPreferences("data",MODE_PRIVATE);
sharedPreferences.edit().putString("url",versionInfoBean.getDownloadUrl()).commit();
sharedPreferences.edit().putString("path",versionInfoBean.getPath()).commit();//getExternalCacheDir获取到的路径 为系统为app分配的内存 卸载app后 该目录下的资源也会删除
//比较版本信息
try {
int result = Utils.compareVersion(Utils.getVersionName(this),versionInfoBean.getVersionName());
if(result==-1){//不是最新版本
showDialog();
}else{
Toast.makeText(UpdateActivity.this,"已经是最新版本",Toast.LENGTH_SHORT).show();
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
private void showDialog(){
final Dialog dialog = new Dialog(UpdateActivity.this);
LayoutInflater inflater = (LayoutInflater) UpdateActivity.this
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
TextView version,content;
Button left,right;
View view = inflater.inflate(R.layout.version_update,null,false);
version = (TextView)view.findViewById(R.id.version);
content = (TextView)view.findViewById(R.id.content);
left = (Button)view.findViewById(R.id.left);
right = (Button)view.findViewById(R.id.right);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
content.setText(Html.fromHtml(versionInfoBean.getDesc(),Html.FROM_HTML_MODE_LEGACY));
}else{
content.setText(Html.fromHtml(versionInfoBean.getDesc()));
}
content.setMovementMethod(LinkMovementMethod.getInstance());
version.setText("版本号:"+versionInfoBean.getVersionName());
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
left.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dialog.dismiss();
}
});
right.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dialog.dismiss();
downloadNewVersionFromServer();
}
});
dialog.setContentView(view);
dialog.setCancelable(false);
Window dialogWindow = dialog.getWindow();
dialogWindow.setGravity(Gravity.CENTER);
//dialogWindow.setWindowAnimations(R.style.ActionSheetDialogAnimation);
WindowManager.LayoutParams lp = dialogWindow.getAttributes();
WindowManager wm = (WindowManager)
getSystemService(Context.WINDOW_SERVICE);
lp.width =wm.getDefaultDisplay().getWidth()/10*9;
dialogWindow.setAttributes(lp);
dialog.show();
}
/**
* 启动服务后台下载
*/
private void downloadNewVersionFromServer(){
if(new File(versionInfoBean.getPath()).exists()){
new File(versionInfoBean.getPath()).delete();
}
Toast.makeText(UpdateActivity.this,"开始下载...",Toast.LENGTH_SHORT).show();
LoadingService.startUploadImg(this);
}
/**
* 定义广播接收者 接受下载状态
*/
public class MyReceive extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if("android.intent.action.loading_over".equals(action)){
isLoading = false;
}else if("android.intent.action.loading".equals(action)){
isLoading = true;
}
}
}
}
工具类三个:
LoadingService.java
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.widget.RemoteViews;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.content.FileProvider;
import com.example.administrator.loginclient.R;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import java.io.File;
public class LoadingService extends IntentService {
private HttpUtils httpUtils;
NotificationManager nm;
private String url,path;
private SharedPreferences sharedPreferences;
public LoadingService(String name) {
super(name);
}
public LoadingService() {
super("MyService");
}
public static void startUploadImg(Context context)
{
Intent intent = new Intent(context, LoadingService.class);
context.startService(intent);
}
public void onCreate() {
super.onCreate();
httpUtils = new HttpUtils();
httpUtils.configCurrentHttpCacheExpiry(0);
nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
sharedPreferences = getSharedPreferences("data",MODE_PRIVATE);
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
updateApk();
}
private void updateApk(){
url = sharedPreferences.getString("url","");
path = sharedPreferences.getString("path","");
httpUtils.download(url,
path , new RequestCallBack<File>() {
@Override
public void onLoading(final long total, final long current,
boolean isUploading) {
createNotification(total,current);
sendBroadcast(new Intent().setAction("android.intent.action.loading"));//发送正在下载的广播
super.onLoading(total, current, isUploading);
}
@Override
public void onSuccess(ResponseInfo<File> arg0) {
nm.cancel(R.layout.notification_item);
Toast.makeText(LoadingService.this,"下载成功...",Toast.LENGTH_SHORT).show();
installApk();//下载成功 打开安装界面
stopSelf();//结束服务
sendBroadcast(new Intent().setAction("android.intent.action.loading_over"));//发送下载结束的广播
}
@Override
public void onFailure(HttpException arg0, String arg1) {
Toast.makeText(LoadingService.this,"下载失败...",Toast.LENGTH_SHORT).show();
sendBroadcast(new Intent().setAction("android.intent.action.loading_over"));//发送下载结束的广播
nm.cancel(R.layout.notification_item);
stopSelf();
}
});
}
/**
* 安装下载的新版本
*/
protected void installApk() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
File file = new File(path);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 7.0 以上
try {
Uri apkUri = FileProvider.getUriForFile(this, "com.jw.fileprovider", file);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
}catch (Exception e){
}
} else {
// 7.0以下
Uri uri = Uri.fromFile(file);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
}
this.startActivity(intent);
}
private void createNotification(final long total, final long current){
NotificationCompat.Builder builder = new NotificationCompat.Builder(this,"default");
builder.setSmallIcon(R.mipmap.ic_launcher)//必须要设置这个属性,否则不显示
;
RemoteViews contentView = new RemoteViews(this.getPackageName(),R.layout.notification_item);
contentView.setProgressBar(R.id.progress, (int)total, (int)current, false);
builder.setOngoing(true);//设置左右滑动不能删除
// Notification notification = builder.build();
// notification.contentView = contentView;
String id = "channel_001";
String name = "name";
Notification notification = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel mChannel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW);
nm.createNotificationChannel(mChannel);
notification = new Notification.Builder(LoadingService.this,"default")
.setChannelId(id)
.setContentTitle("客户端更新")
.setContentText("更新完毕")
.setSmallIcon(R.mipmap.ic_launcher).build();
notification.contentView = contentView;
} else {
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(LoadingService.this,"default")
.setContentTitle("更新了")
.setContentText("更新了xxxxxxxxx内容")
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
;//无效
notification = notificationBuilder.build();
notification.contentView = contentView;
}
nm.notify(R.layout.notification_item,notification);//发送通知
}
}
Utils.java
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
public class Utils {
/**
* 获取版本号
*
* @throws PackageManager.NameNotFoundException
*/
public static String getVersionName(Context context) throws PackageManager.NameNotFoundException {
// 获取packagemanager的实例
PackageManager packageManager = context.getPackageManager();
// getPackageName()是你当前类的包名,0代表是获取版本信息
PackageInfo packInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
String version = packInfo.versionName;
return version;
}
/**
* 版本号比较
*0代表相等,1代表version1大于version2,-1代表version1小于version2
* @param version1
* @param version2
* @return
*/
public static int compareVersion(String version1, String version2) {
if (version1.equals(version2)) {
return 0;
}
String[] version1Array = version1.split("\\.");
String[] version2Array = version2.split("\\.");
int index = 0;
// 获取最小长度值
int minLen = Math.min(version1Array.length, version2Array.length);
int diff = 0;
// 循环判断每位的大小
while (index < minLen
&& (diff = Integer.parseInt(version1Array[index])
- Integer.parseInt(version2Array[index])) == 0) {
index++;
}
if (diff == 0) {
// 如果位数不一致,比较多余位数
for (int i = index; i < version1Array.length; i++) {
if (Integer.parseInt(version1Array[i]) > 0) {
return 1;
}
}
for (int i = index; i < version2Array.length; i++) {
if (Integer.parseInt(version2Array[i]) > 0) {
return -1;
}
}
return 0;
} else {
return diff > 0 ? 1 : -1;
}
}
}
VersionInfoBean.java
public class VersionInfoBean {
private String versionName;
private String downloadUrl;
private String desc;
private String path;
public VersionInfoBean(String versionName, String downloadUrl, String desc, String path) {
this.versionName = versionName;
this.downloadUrl = downloadUrl;
this.desc = desc;
this.path = path;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getVersionName() {
return versionName;
}
public void setVersionName(String versionName) {
this.versionName = versionName;
}
public String getDownloadUrl() {
return downloadUrl;
}
public void setDownloadUrl(String downloadUrl) {
this.downloadUrl = downloadUrl;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public VersionInfoBean(String versionName, String downloadUrl, String desc) {
this.versionName = versionName;
this.downloadUrl = downloadUrl;
this.desc = desc;
}
}
layout下的xml
activity_update.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".Activities.UpdateActivity">
<!--
版本更新页面
-->
<Button
android:id="@+id/btn_refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="版本更新"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
version_update.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!--
安装页面
-->
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical|center_horizontal"
android:paddingLeft="10dp"
android:text="更新提示"
android:textColor="#000000"
android:textSize="18sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000" />
<TextView
android:layout_marginTop="10dp"
android:id="@+id/version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="10dp"
android:text="content"
android:textSize="16sp" />
<TextView
android:layout_marginTop="14dp"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="10dp"
android:text="content"
android:textSize="14sp" />
<TextView
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#f0f0f0" />
<LinearLayout
android:layout_marginTop="4dp"
android:layout_width="match_parent"
android:layout_height="45dp"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<Button
android:textColor="#000000"
android:id="@+id/left"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@null"
android:gravity="center_vertical|center_horizontal"
android:text="取消"
android:textSize="16sp"
/>
<TextView
android:textColor="#f0f0f0"
android:id="@+id/line"
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
/>
<Button
android:textColor="#d90c0c"
android:id="@+id/right"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:layout_weight="1"
android:background="@null"
android:gravity="center_vertical|center_horizontal"
android:text="更新"
android:textSize="16sp"
/>
</LinearLayout>
</LinearLayout>
notification_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal" android:layout_width="match_parent"
android:paddingTop="8dp"
android:layout_height="wrap_content">
<!--
下载通知页面-->
<ImageView
android:src="@mipmap/ic_launcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_marginRight="6dp"
android:layout_marginLeft="8dp"
android:layout_gravity="center_vertical"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textSize="14sp"
android:text="正在下载"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ProgressBar
android:layout_marginTop="4dp"
style="?android:attr/progressBarStyleHorizontal"
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
在res新建一个xml文件夹
在xml文件夹里新建资源文件update_apk_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path name="update" path="." />
</paths>
</resources>
在xml文件夹里新建资源文件network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
添加的依赖:xUtils-2.6.14.jar 提取码:p5q3
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.core:core:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation files('libs/xUtils-2.6.14.jar')
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
exclude group: 'com.android.support', module: 'support-annotations'
})
manifest清单
1.在<manifest > ...</manifest>
里面加上权限:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
2.在 <application> ... </application>
里面加上:
android:networkSecurityConfig="@xml/network_security_config"
<!--Android 9.0加的-->
<uses-library
android:name="org.apache.http.legacy"
android:required="false"
/>
<!--Activities是我的包名-->
<receiver android:name=".Activities.UpdateActivity$MyReceive">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPETE"/>
</intent-filter>
</receiver>
<activity android:name=".Activities.UpdateActivity"
tools:ignore="InnerclassSeparator">
</activity>
<service
<!-- .Updates.LoadingService里的 Updates是我的包名-->
android:name=".Updates.LoadingService"
android:process="system"
tools:ignore="InnerclassSeparator" />
<!-- 注册我系统级别的服务 这样退出app可以继续下载 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.jw.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<!-- 元数据 -->
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_apk_paths" />
</provider>
(2)Tomcat的配置:
<?xml version='1.0' encoding='utf-8'?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Note: A "Server" is not itself a "Container", so you may not
define subcomponents such as "Valves" at this level.
Documentation at /docs/config/server.html
-->
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<!-- Security listener. Documentation at /docs/config/listeners.html
<Listener className="org.apache.catalina.security.SecurityListener" />
-->
<!--APR library loader. Documentation at /docs/apr.html -->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html -->
<Listener className="org.apache.catalina.core.JasperListener" />
<!-- Prevent memory leaks due to use of particular java/javax APIs-->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- Global JNDI resources
Documentation at /docs/jndi-resources-howto.html
-->
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<!-- A "Service" is a collection of one or more "Connectors" that share
a single "Container" Note: A "Service" is not itself a "Container",
so you may not define subcomponents such as "Valves" at this level.
Documentation at /docs/config/service.html
-->
<Service name="Catalina">
<!--The connectors can use a shared executor, you can define one or more named thread pools-->
<!--
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
-->
<!-- A "Connector" represents an endpoint by which requests are received
and responses are returned. Documentation at :
Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)
Java AJP Connector: /docs/config/ajp.html
APR (HTTP/AJP) Connector: /docs/apr.html
Define a non-SSL HTTP/1.1 Connector on port 8080
-->
<Connector port="8083" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="443" />
<!-- A "Connector" using the shared thread pool-->
<!-- <Connector executor="tomcatThreadPool"
port="8083" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="443" />
-->
<!-- Define an SSL HTTP/1.1 Connector on port 8443
This connector uses the BIO implementation that requires the JSSE
style configuration. When using the APR/native implementation, the
OpenSSL style configuration is required as described in the APR/native
documentation -->
<Connector port="443" protocol="org.apache.coyote.http11.Http11Protocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS" />
<!-- Define an AJP 1.3 Connector on port 8009 -->
<!--
<Connector protocol="AJP/1.3"
address="::1"
port="8009"
redirectPort="8443" />
-->
<!-- An Engine represents the entry point (within Catalina) that processes
every request. The Engine implementation for Tomcat stand alone
analyzes the HTTP headers included with the request, and passes them
on to the appropriate Host (virtual host).
Documentation at /docs/config/engine.html -->
<!-- You should set jvmRoute to support load-balancing via AJP ie :
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
-->
<Engine name="Catalina" defaultHost="localhost">
<!--For clustering, please take a look at documentation at:
/docs/cluster-howto.html (simple how to)
/docs/config/cluster.html (reference documentation) -->
<!--
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
-->
<!-- Use the LockOutRealm to prevent attempts to guess user passwords
via a brute-force attack -->
<Realm className="org.apache.catalina.realm.LockOutRealm">
<!-- This Realm uses the UserDatabase configured in the global JNDI
resources under the key "UserDatabase". Any edits
that are performed against this UserDatabase are immediately
available for use by the Realm. -->
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<!-- SingleSignOn valve, share authentication between web applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
-->
<!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log." suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
<Context path="/PersonalHomePage" docBase="D:\PersonalHomePage" debug="0" reloadable="true" />
</Host>
</Engine>
</Service>
</Server>
关于server.xml具体增加了什么操作?
其实是在 <Host>... </Host>
里面加了以下的映射:
<Context path="/PersonalHomePage" docBase="D:\PersonalHomePage" debug="0" reloadable="true" />
在电脑的D盘里,新建一个名为PersonalHomePage的文件夹。
然后再Android studio里build apk,把.apk文件复制粘贴到PersonalHomePage里。文件名字不用更改了,因为我在代码里已经把该名字写好了。
最后打开Tomcat,再运行APP就行了。
注意:以上的实现是基于androidx。