在软件的开发和维护过程中,软件是需要不断更新的,不可能一开始就有好的软件,尤其是精品软件的形成。那么如何让用户第一时间获取最新的应用安装包呢?那么就要求我们从第一个版本就要实现升级模块这一功能。
自动更新功能的实现原理,就是我们事先和后台协商好一个接口,我们在应用的主Activity里,去访问这个接口,如果需要更新,后台会返回一些数据(比 如,提示语;最新版本的url等)。然后我们给出提示框,用户点击开始下载,下载完成开始覆盖安装程序,这样用户的应用就会保持最新。
为了使大家更加容易理解,我把项目中的更新事例给大家展示下。我们把软件更新放在用户登陆的界面。
第一步:新建一个用户登陆的布局文件login.xml<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@drawable/main_bg"
android:orientation="vertical" >
<ScrollView
android:id="@+id/Main_LinearLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/bottommenu"
android:layout_alignParentTop="true" >
<LinearLayout
android:id="@+id/Main_LinearLayout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/TitleBG"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bg" >
</ImageView>
<TextView
android:id="@+id/system_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="20dip"
android:text="@string/login_name"
android:textColor="@drawable/black"
android:textSize="30sp" >
</TextView>
<TableLayout
android:id="@+id/TableLayout01"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="20dip"
android:paddingRight="20dip"
android:paddingTop="20dip"
android:shrinkColumns="1"
android:stretchColumns="1" >
<TableRow android:paddingTop="15dip" >
<TextView
android:id="@+id/TextView01"
android:padding="10dip"
android:text="@string/user_code_text"
android:textColor="@drawable/black"
android:textSize="18sp" >
</TextView>
<EditText
android:id="@+id/user_code"
android:hint="@string/username_hint"
android:padding="10dip"
android:textSize="16sp" />
</TableRow>
<TableRow android:paddingTop="15dip" >
<TextView
android:id="@+id/TextView02"
android:padding="10dip"
android:text="@string/password_text"
android:textColor="@drawable/black"
android:textSize="18sp" >
</TextView>
<EditText
android:id="@+id/user_pwd"
android:hint="@string/password_hint"
android:padding="10dip"
android:password="true"
android:textSize="16sp" />
</TableRow>
<TableRow
android:gravity="center"
android:paddingTop="10dip" >
<CheckBox
android:id="@+id/remember_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/rememberpwd_text"
android:textColor="@drawable/black" />
</TableRow>
</TableLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@id/bottommenu"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
android:padding="10dip" >
<Button
android:id="@+id/login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/login_text" >
</Button>
<Button
android:id="@+id/exit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/exit_text" >
</Button>
</LinearLayout>
</RelativeLayout>
第二步:建立业务类文件LoginActions.java,这个类主要用来登陆
package org.DigitalCM;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.Base.Activities.ContextActions;
import org.Base.Activities.ContextActivity;
import org.Base.Utils.PropertyUtil;
import org.Base.Webservice.WSObjectMapUtil;
import org.Base.Webservice.WSObjectUtil;
import org.Base.Webservice.WSUtil;
import org.Base.Webservice.WebServiceConfig;
import org.DigitalCM.entities.UserInfo;
import org.ksoap2.serialization.SoapObject;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
public class LoginActions extends ContextActions {
public String SDPATH;
public int totalSize = 0;
public int downloadedSize = 0;
public URL url = null;
public final static int PROGRESS_DIALOG = 1;
public final static int DIALOG_LOGIN_FAIELD = 2;
public final static int DIALOG_CONNECT_ERROR = 3;
public final static int DIALOG_USER_PWD_EMPTY = 4;
public final static int DIALOG_EXIT_PROMPT = 5;
public final static int DIALOG_VERSION_UPDATE = 6;
boolean bNeedUpdate = false;
private Button login = null;
private Button exit = null;
private EditText userCode = null;
private EditText password = null;
private CheckBox rememberpwd = null;
// private CheckBox outlineLogin = null;
public LoginActions(ContextActivity contextActivity) {
// TODO Auto-generated constructor stub
super(contextActivity);
SDPATH = Environment.getExternalStorageDirectory() + "/";
}
public void setOnClickListener() {
/* 为 Button 注册点击事件监听对象,采用了匿名内部类的方式。 */
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
boolean flag = false;
flag = login();
if (flag) {
loginaction();
}
}
});
/* 为 Button 注册点击事件监听对象,采用了匿名内部类的方式。 */
exit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
exitApp();
}
});
}
public void findViews() {
userCode = (EditText) contextActivity.findViewById(R.id.user_code);
password = (EditText) contextActivity.findViewById(R.id.user_pwd);
rememberpwd = (CheckBox) contextActivity
.findViewById(R.id.remember_pwd);
login = (Button) contextActivity.findViewById(R.id.login);
exit = (Button) contextActivity.findViewById(R.id.exit);
// outlineLogin = (CheckBox)
// contextActivity.findViewById(R.id.outline_login);
}
public String result = null;
private void loginaction() {
String version = null;
PackageManager pm = contextActivity.getPackageManager();
PackageInfo info = null;
try {
info = pm.getPackageInfo("org.DigitalCM",
PackageManager.GET_ACTIVITIES);
} catch (NameNotFoundException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
// PackageInfo info = pm.getPackageArchiveInfo(archiveFilePath,
// PackageManager.GET_ACTIVITIES);
if (info != null) {
// ApplicationInfo appInfo = info.applicationInfo;
// String appName = pm.getApplicationLabel(appInfo).toString();
// String packageName = appInfo.packageName; // 得到安装包名称
version = info.versionName; // 得到版本信息
}
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("version", version);
Object loginResult = null;
try {
loginResult = WSUtil.getObjectByCallingWS(
WebServiceConfig.NAMESPACE, "versionupdate", params,
WebServiceConfig.wsdl);
result = loginResult.toString();
} catch (Exception e) {
contextActivity.showDialog(DIALOG_CONNECT_ERROR);
Log.e("Exception", e.getMessage());
return;
}
if (result == null
|| (result != null && result.equals("notneedupdate"))) {
loginSuccess();
} else {
contextActivity.showDialog(DIALOG_VERSION_UPDATE);
}
}
@SuppressWarnings("static-access")
public boolean login() {
if (!validate(userCode.getText().toString(), password.getText()
.toString()))
return false;
Map<String, Object> params = new HashMap<String, Object>();
params.put("userName", userCode.getText().toString().trim()
.toLowerCase());
params.put("userPwd", password.getText().toString());
SoapObject result = null;
try {
result = WSUtil.getSoapObjectByCallingWS(
WebServiceConfig.NAMESPACE, "login", params,
WebServiceConfig.wsdl);
} catch (Exception e) {
contextActivity.showDialog(DIALOG_CONNECT_ERROR);
Log.e("Exception", e.getMessage());
return false;
}
if (result == null) {
loginFailed();
return false;
}
WSObjectUtil wsObjectUtil = new WSObjectUtil();
SoapObject dataSet;
try {
dataSet = wsObjectUtil.getDataSetObject(result);
} catch (Exception e) {
loginFailed();
return false;
}
if (dataSet == null) {
loginFailed();
return false;
}
List<Map<String, Object>> rowMapList = WSObjectMapUtil
.getRowMapList(dataSet);
UserInfo.setUserInfo(rowMapList.get(0));
return true;
}
public void loginSuccess() {
// TODO Auto-generated method stub
Intent intent = null;
if (UserInfo.getDeptID() != null && UserInfo.getDeptID().equals("12")) {// 社区领导(报送物业、上报上级、结案评价)
intent = new Intent(contextActivity, MainCommunity.class);
} else if (UserInfo.getDeptID() != null
&& UserInfo.getDeptID().equals("17")) {// 网格责任人(问题上报、核查)
intent = new Intent(contextActivity, Main.class);
} else if ((UserInfo.getDeptID() != null && UserInfo.getDeptID()
.equals("41"))
|| (UserInfo.getDeptID() != null && UserInfo.getDeptID()
.toString().equals("11"))
|| (UserInfo.getDeptID() != null && UserInfo.getDeptID()
.toString().equals("411"))) {// 街道领导,区监督指挥中心(受理中心)
intent = new Intent(contextActivity, MainAccept.class);
} else if (UserInfo.getDeptID() != null
&& UserInfo.getDeptID().equals("61")) {// 处置部门(街道-区级事务用户)
intent = new Intent(contextActivity, MainDispose.class);
}
contextActivity.startActivity(intent);
contextActivity.finish();
return;
}
private Boolean validate(String user_code, String password) {
if (user_code == null || user_code.trim().equals("")
|| password == null || password.trim().equals("")) {
contextActivity.showDialog(DIALOG_USER_PWD_EMPTY);
return false;
}
return true;
}
public void loadConfig() {
Properties properties = PropertyUtil
.loadConfig(WebServiceConfig.CONFIG_PATH);
String username = properties.getProperty("username");
String pwd = properties.getProperty("password");
properties = PropertyUtil.loadConfig(WebServiceConfig.CONFIG_PATH);
username = properties.getProperty("username");
pwd = properties.getProperty("password");
if (pwd != null && !pwd.equals("")) {
rememberpwd.setChecked(true);
password.setText(pwd);
} else {
rememberpwd.setChecked(false);
}
userCode.setText(username);
}
private void loginFailed() {
contextActivity.showDialog(DIALOG_LOGIN_FAIELD);
}
private void exitApp() {
contextActivity.showDialog(DIALOG_EXIT_PROMPT);
}
/**
* 在SD卡上创建文件
*/
public File creatSDFile(String fileName) throws IOException {
File file = new File(SDPATH + fileName);
file.createNewFile();
return file;
}
/**
* 在SD卡上删掉相应目录文件
*/
public Boolean deleteSDFile(String fileName) throws IOException {
File file = new File(SDPATH + fileName);
if (file.exists()) {
File filelist[] = file.listFiles();
for (int index = 0; index < filelist.length; index++) {
filelist[index].delete();
}
} else {
file.mkdir();
}
return true;
}
/**
* 在SD卡上创建目录
*/
public File creatSDDir(String dirName) {
File dir = new File(SDPATH + dirName);
dir.mkdir();
return dir;
}
/**
* 判断SD卡上的文件夹是否存在
*/
public boolean isFileExist(String fileName) {
File file = new File(SDPATH + fileName);
return file.exists();
}
/**
* 根据URL得到输入流
*/
public InputStream getInputStreamFromUrl(String urlStr)
throws MalformedURLException, IOException {
url = new URL(urlStr);// 创建一个URL对象
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();// 创建一个Http连接
totalSize = urlConn.getContentLength();
InputStream inputStream = urlConn.getInputStream();// 得到输入流
return inputStream;
}
/**
* 根据URL下载文件,前提是这个文件当中的内容是文本,函数的返回值就是文件当中的内容 1.创建一个URL对象
* 2.通过URL对象,创建一个HttpURLConnection对象 3.得到InputStram 4.从InputStream当中读取数据
*/
public String downStr(String urlStr)// 下载字符流的方法
{
/**
* String和StringBuffer他们都可以存储和操作字符串,即包含多个字符的字符串数据。
* String类是字符串常量,是不可更改的常量。而StringBuffer是字符串变量,它的对象是可以扩充和修改的。
*/
StringBuffer sb = new StringBuffer();
String line = null;
BufferedReader buffer = null;// BufferedReader类用于从缓冲区中读取内容
try {
/**
* 因为直接使用InputStream不好用,多以嵌套了BufferedReader,这个是读取字符流的固定格式。
*/
url = new URL(urlStr);// 创建一个URL对象
HttpURLConnection urlConn = (HttpURLConnection) url
.openConnection();// 创建一个Http连接
buffer = new BufferedReader(new InputStreamReader(
urlConn.getInputStream()));// 使用IO流读取数据
while ((line = buffer.readLine()) != null) {
sb.append(line);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
buffer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
第三步:新建一个Activity
package org.DigitalCM;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.Base.Activities.ContextActivity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.app.SearchManager.OnCancelListener;
import android.app.SearchManager.OnDismissListener;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
public class DigitalCity extends ContextActivity implements OnClickListener {
private LoginActions loginActivities = new LoginActions(this);
private ProgressDialog mProgressDialog = null;
private ProcessThread mThread;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.login);
loginActivities.findViews();
loginActivities.loadConfig();
loginActivities.setOnClickListener();
}
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
@Override
protected Dialog onCreateDialog(int id) {
// TODO Auto-generated method stub
switch (id) {
case LoginActions.PROGRESS_DIALOG:
mThread = new ProcessThread();
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setTitle("提示"); // 设置标题
mProgressDialog.setMessage("正在下载文件"
+ loginActivities.result.replace("png", "apk") + ",请稍候..."); // 设置body信息
mProgressDialog.setMax(100); // 进度条最大值是100
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); // 设置进度条样式是
// mProgressDialog.setOnCancelListener(mThread);
// mProgressDialog.setOnDismissListener(mThread);
// mProgressDialog.setButton("取消", mThread);
mProgressDialog.setCancelable(false);
mThread.start();
return mProgressDialog;
case LoginActions.DIALOG_LOGIN_FAIELD:
return new AlertDialog.Builder(this)
.setTitle(R.string.error_title)
.setMessage(R.string.login_failed)
.setPositiveButton(R.string.OK_text,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int arg1) {
// TODO Auto-generated method stub
dialog.cancel();
}
}).show();
case LoginActions.DIALOG_CONNECT_ERROR:
return new AlertDialog.Builder(this)
.setTitle(R.string.message_title)
.setMessage(R.string.connection_error)
.setPositiveButton(R.string.OK_text,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// TODO Auto-generated method stub
dialog.cancel();
}
}).show();
case LoginActions.DIALOG_USER_PWD_EMPTY:
return new AlertDialog.Builder(this)
.setTitle(R.string.message_title)
.setMessage(R.string.usercode_or_password_empty_warning)
.setPositiveButton(R.string.OK_text,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// TODO Auto-generated method stub
dialog.cancel();
}
}).show();
case LoginActions.DIALOG_EXIT_PROMPT:
return new AlertDialog.Builder(this)
.setTitle(R.string.message_title)
.setMessage(R.string.exit_prompt)
.setPositiveButton(R.string.OK_text,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
finish();
}
})
.setNegativeButton(R.string.cancel_text,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
dialog.cancel();
}
}).show();
case LoginActions.DIALOG_VERSION_UPDATE:
return new AlertDialog.Builder(this)
.setTitle(R.string.message_title)
.setMessage("此软件有新版本,需要更新,是否要下载新版本?")
.setPositiveButton(R.string.OK_text,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// TODO Auto-generated method stub
showDialog(LoginActions.PROGRESS_DIALOG);
}
})
.setNegativeButton(R.string.cancel_text,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// TODO Auto-generated method stub
loginActivities.loginSuccess();
return;
}
}).show();
}
return super.onCreateDialog(id);
}
private class ProcessThread extends Thread implements OnCancelListener,
OnDismissListener, OnClickListener {
public void run() {
try {
loginActivities.deleteSDFile("voa/");
} catch (IOException e1) {
e1.printStackTrace();
}
// 下载文件路径,存储文件名,
// 220.178.224.44
String downfile = "http://192.168.0.2/webservices/apk/"
+ loginActivities.result;
downFile(downfile, "voa/", "DigitalCity.apk");
}
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
@Override
public void onDismiss() {
// TODO Auto-generated method stub
}
@Override
public void onCancel() {
// TODO Auto-generated method stub
}
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {// 定义一个Handler,用于处理下载线程与UI间通讯
if (!Thread.currentThread().isInterrupted()) {
switch (msg.what) {
case 0:
mProgressDialog.setMax(loginActivities.totalSize);
case 1:
mProgressDialog.setProgress(loginActivities.downloadedSize);
break;
case 2:
loginActivities.downloadedSize = 0;
mProgressDialog.dismiss();
removeDialog(LoginActions.PROGRESS_DIALOG);
AlertDialog.Builder dialog = new AlertDialog.Builder(
DigitalCity.this);
dialog.setTitle("提示")
.setMessage("下载完成,请安装!")
.setPositiveButton("确认",
new DialogInterface.OnClickListener() {
@Override
public void onClick(
DialogInterface dialog,
int which) {
// TODO Auto-generated method stub
Uri data = Uri.parse("file://"
+ loginActivities.SDPATH
+ "voa/"
+ "DigitalCity.apk");
// 调用系统的安装方法
Intent intent = new Intent(
Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(data,
"application/vnd.android.package-archive");
DigitalCity.this
.startActivity(intent);
}
});
dialog.show();
removeCallbacks(mThread);
// android.os.Process.killProcess(android.os.Process.myPid());
break;
case -1:
break;
}
}
super.handleMessage(msg);
}
};
/**
* 将一个InputStream里面的数据写入到SD卡中
*/
public File write2SDFromInput(String path, String fileName,
InputStream input) {
File file = null;
OutputStream output = null;
try
// InputStream里面的数据写入到SD卡中的固定方法
{
loginActivities.creatSDDir(path);
file = loginActivities.creatSDFile(path + fileName);
output = new FileOutputStream(file);
byte buffer[] = new byte[4 * 1024];
int bufferLength = 0;
sendMsg(0);
while ((bufferLength = input.read(buffer)) != -1) {
output.write(buffer, 0, bufferLength);
loginActivities.downloadedSize += bufferLength;
sendMsg(1);// 更新进度条
}
sendMsg(2);// 通知下载完成
output.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
output.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return file;
}
private void sendMsg(int flag) {
Message msg = new Message();
msg.what = flag;
handler.sendMessage(msg);
}
/**
* -1:代表下载文件出错 0:代表下载文件成功 1:代表文件已经存在
*/
public int downFile(String urlStr, String path, String fileName)// 下载文件的方法
{
InputStream inputStream = null;
try {
loginActivities.deleteSDFile(path);
if (loginActivities.isFileExist(path + fileName)) {
return 1;
} else {
inputStream = loginActivities.getInputStreamFromUrl(urlStr);
File resultFile = write2SDFromInput(path, fileName, inputStream);
if (resultFile == null) {
return -1;
}
}
} catch (Exception e) {
e.printStackTrace();
return -1;
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
return 0;
}
@Override
protected void onStop() {
// TODO Auto-generated method stub
super.onStop();
}
@Override
protected void setOnClickListener() {
// TODO Auto-generated method stub
}
}
第四步:添加程序所用的权限:
<uses-permission android:name="android.permission.INTERNET" />