原先想使用Push的方法實踐APP自動更新, 但在研究Google的C2DM之後, 發現想要以此方法實踐, 必須要有Android Market帳號, 註冊費US$25, 而且必須將APP放在公開的Android Market上, 這對企業內部開發的程序似乎不是很好的解決方案, 後來同事給我這篇文章Android应用程序的自动更新升级, 我就是試著以IIS實踐並修改部分程序, 流程大致如: 檢查網路>透過HTTP取得遠端的APP版本資訊>取得手機APP版本資訊>比較兩者>若不符就先下載新版APP至SDCard>再取代舊版>最後使用者必須手動開啟新版APP
原先想使用Push的方法實踐APP自動更新, 但在研究Google的C2DM之後, 發現想要以此方法實踐, 必須要有Android Market帳號, 註冊費US$25, 而且必須將APP放在公開的Android Market上, 這對企業內部開發的程序似乎不是很好的解決方案, 後來同事給我這篇文章Android应用程序的自动更新升级, 我就是試著以IIS實踐並撰寫MainMenu
流程大致如: 檢查網路>透過HTTP取得所有APP最新版本資訊>取得裝置APP版本資訊>比較兩者>
若不符就顯示下載或更新訊息, 再將新版APP儲存至SDCard, 安裝後自動開啟新APP, 結束MainMenu
若無不符就自動開啟所選的APP, 結束MainMenu
STP1. 編輯version.json下, VerName是給使用者看的版本, VerCode是開發者用的內部版本
STP2. 部屬version.json至IIS, 並設定MIME接受.json及.apk
STP3. Eclipse, Project>Proerties>JavaBuildPath>Libraries>AddExternalJAR..>json.jar>OK
STP4. 建立AndroidProject:MAinMenu, 設定APP/SDK版本及Permission如下,
\res\values\strings.xml, 新增String供MainActivity顯示版本資訊
<string name="act_title">MainMenu (1.0.1)</string>
AndroidManifest.xml, 設定versionCode, versionName, SdkVersion, permission
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="quanta.shopfloor.mainmenu"
android:versionCode="20130513"
android:versionName="1.0.1" >
<uses-sdk
android:minSdkVersion="9"
android:targetSdkVersion="17" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="quanta.shopfloor.mainmenu.MainActivity"
android:label="@string/act_title" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
STP5. \res\layout\activity_main.xml, 宣告lvAppMenu展現HTTP取回的所有APP最新版本資訊
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/lvAppMenu"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
<TableLayout
android:id="@+id/tlMsg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@android:color/black" >
<TableRow
android:id="@+id/tableRow1"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:text="MSG "
android:textColor="#FFFFFF"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
android:paddingRight="10dp"
android:paddingTop="10dp"
android:textColor="#FF0000"
android:textSize="16sp"
android:textStyle="bold" />
</TableRow>
</TableLayout>
</RelativeLayout>
STP6. \src\quanta\shopfloor\mainmenu\Parameters.java, 宣告全域變數
package quanta.shopfloor.mainmenu;
public class Parameters {
public static String strServerPath = "http://192.168.20.14/ver/version.json";
public static String strHttpAppInfo = "";
public static String strSelAppName = "";
public static String strSelAppDwnUrl = "";
public static String strSelPakAppName = "";
public static String strSelAppMsg = "";
public static String strExceptionMsg;
}
STP7. \src\quanta\shopfloor\mainmenu\MainActivity.java, 程序流程: 流程大致如: 檢查網路>透過HTTP取得所有APP最新版本資訊>取得裝置APP版本資訊>比較兩者>若不符就顯示下載或更新訊息, 再將新版APP儲存至SDCard, 安裝後自動開啟新APP, 結束MainMenu; 若無不符就自動開啟所選的APP, 結束MainMenu; chkNetwork必須開啟android.permission.ACCESS_NETWORK_STATE
package quanta.shopfloor.mainmenu;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.json.JSONObject;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TableLayout;
import android.widget.TextView;
public class MainActivity extends Activity {
private ListView lvAppMenu;
private TableLayout tlMsg;
private TextView tvMsg;
private String[] lvAppName;
private String[] lvHttpDwnUrl;
private String[] lvPakName;
private String[] lvAppMsg;
private PackageManager packageManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
lvAppMenu = (ListView) findViewById(R.id.lvAppMenu);
tlMsg = (TableLayout)findViewById(R.id.tlMsg);
tvMsg = (TextView)findViewById(R.id.tvMsg);
if(!chkNetwork()){
tvMsg.setText("NetworkNotAvailable");
}else{
tvMsg.setText("");
tlMsg.setVisibility(TableLayout.GONE);
getHttpAppInfo();
getAppMenu();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.add(Menu.NONE, 0, 0, "Quit");
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case 0:
this.finish();
break;
default:
break;
}
return false;
}
private boolean chkNetwork(){
try{
ConnectivityManager connectivityManager = (ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
if(networkInfo != null && networkInfo.isAvailable()){
return true;
}else{
return false;
}
}catch(Exception e){
return false;
}
}
private void getHttpAppInfo(){
try{
Thread_getHttpAppInfo threadHttpAppInfo = new Thread_getHttpAppInfo();
threadHttpAppInfo.start();
threadHttpAppInfo.join();
//tvMsg.setText(Parameters.strHttpAppInfo);
}catch(Exception e){
tvMsg.setText(e.getMessage());
tlMsg.setVisibility(TableLayout.VISIBLE);
}
}
private void getAppMenu(){
try{
JSONObject joHttpAppInfo = new JSONObject(Parameters.strHttpAppInfo);
int rows = joHttpAppInfo.getJSONArray("App").length();
lvAppName = new String[rows];
lvHttpDwnUrl = new String[rows];
Drawable[] lvAppIcon = new Drawable[rows];
lvPakName = new String[rows];
String[] lvHttpAppVer = new String[rows];
String[] lvPhoneAppVer = new String[rows];
lvAppMsg= new String[rows];
ArrayList> arrayList = new ArrayList>();
MyAdapter myAdapter = new MyAdapter(this, arrayList, R.layout.simple_list_item_2,
new String[]{"appIcon", "appName", "pakName", "httpAppVer", "phoneAppVer", "appMsg"},
new int[]{R.id.ivAppIcon, R.id.tvAppName, R.id.tvPakName, R.id.tvHttpAppVer, R.id.tvPhoneAppVer, R.id.tvAppMsg});
for(int i=0; i listApplicationInfo = packageManager.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES);
Collections.sort(listApplicationInfo, new ApplicationInfo.DisplayNameComparator(packageManager));
for (ApplicationInfo applicationInfo : listApplicationInfo) {
if (applicationInfo.packageName.contains("."+appName.toLowerCase())){
getPhoneAppInfo = true;
PackageInfo packageInfo = getPackageManager().getPackageInfo(applicationInfo.packageName, 0);
lvAppIcon[i] = applicationInfo.loadIcon(packageManager);
lvPakName[i] = applicationInfo.packageName;
lvPhoneAppVer[i] = "Current Version: "+packageInfo.versionName +" ("+packageInfo.versionCode+")";
}
}
if(!getPhoneAppInfo){
lvAppIcon[i] = getResources().getDrawable(R.drawable.ic_launcher);
lvPakName[i] = "";
lvPhoneAppVer[i] = "";
}
if(lvPhoneAppVer[i].equals("")){
lvAppMsg[i] = "install";
}else if(!lvHttpAppVer[i].replace("Latest", "").equals(lvPhoneAppVer[i].replace("Current", ""))){
lvAppMsg[i] = "update";
}else{
lvAppMsg[i] = "";
}
HashMap hashMap = new HashMap();
hashMap.put("appIcon", lvAppIcon[i]);
hashMap.put("appName", lvAppName[i]);
hashMap.put("pakName", lvPakName[i]);
hashMap.put("httpAppVer", lvHttpAppVer[i]);
hashMap.put("phoneAppVer", lvPhoneAppVer[i]);
hashMap.put("appMsg", lvAppMsg[i]);
arrayList.add(hashMap);
//Log.v("gv", gvAppName[i]+";"+gvHttpAppVer[i]+";"+gvPhoneAppVer[i]);
}
lvAppMenu.setAdapter(myAdapter);
lvAppMenu.setOnItemClickListener(onItemClickListener);
}catch(Exception e){
tvMsg.setText(e.getMessage());
tlMsg.setVisibility(TableLayout.VISIBLE);
}
}
public OnItemClickListener onItemClickListener = new OnItemClickListener(){
@Override
public void onItemClick(AdapterView parent, View v, int position, long id) {
Parameters.strSelAppName = lvAppName[position];
Parameters.strSelAppDwnUrl = lvHttpDwnUrl[position];
Parameters.strSelPakAppName = lvPakName[position];
Parameters.strSelAppMsg = lvAppMsg[position];
//tvMsg.setText(Parameters.strSelAppName+";"+Parameters.strSelAppDwnUrl+";"+Parameters.strSelPakAppName+";"+Parameters.strSelAppMsg);
StrApk();
}
};
private void StrApk(){
try{
if(!Parameters.strSelAppMsg.equals("")){
//Download
Thread_dwnApk dwnApk = new Thread_dwnApk();
dwnApk.start();
dwnApk.join();
//Install
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(Environment.getExternalStorageDirectory(), Parameters.strSelAppName+".apk")),"application/vnd.android.package-archive");
startActivity(intent);
}else{
Intent intent = getPackageManager().getLaunchIntentForPackage(Parameters.strSelPakAppName);
startActivity(intent);
}
this.finish();
}catch(Exception e){
tvMsg.setText(e.getMessage());
tlMsg.setVisibility(TableLayout.VISIBLE);
}
}
} /hashmap
STP8. \src\quanta\shopfloor\mainmenu\Thread_getHttpAppInfo, 透過HTTP存取記有版本資訊的JSON文件, 因此HTTP的存取必須開啟android.permission.INTERNET並使用Thread
package quanta.shopfloor.mainmenu;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
public class Thread_getHttpAppInfo extends Thread implements Runnable {
public void run() {
try{
StringBuilder HttpAppInfo = new StringBuilder();
HttpGet httpGet = new HttpGet(Parameters.strServerPath);
HttpClient httpClient = new DefaultHttpClient();
HttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity httpEntity = httpResponse.getEntity();
if(httpEntity != null){
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpEntity.getContent(),"UTF-8"),8192);
String line = null;
while((line = bufferedReader.readLine()) != null){
HttpAppInfo.append(line+"\n");
}
bufferedReader.close();
}
Parameters.strHttpAppInfo = HttpAppInfo.toString();
}catch(Exception e){
Parameters.strExceptionMsg = e.getMessage();
}
}
}
STP9. \src\quanta\shopfloor\mainmenu\Thread_dwnApk.java, 透過HTTP下載APK至SDCard, 因此必須開啟android.permission.WRITE_EXTERNAL_STORAGE, 否則會出現/storage/sdcard0/AppVerAutoUpd.apk: open failed: EACCES (Permission denied)
package quanta.shopfloor.mainmenu;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import android.os.Environment;
public class Thread_dwnApk extends Thread implements Runnable {
public void run() {
try{
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet(Parameters.strSelAppDwnUrl);
HttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity httpEntity = httpResponse.getEntity();
InputStream is = httpEntity.getContent();
if(is != null){
File file = new File(Environment.getExternalStorageDirectory(), Parameters.strSelAppName+".apk");
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] buf = new byte[1024];
int ch=-1;
do{
ch = is.read(buf);
if(ch<=0) break;
fileOutputStream.write(buf, 0, ch);
}while(true);
fileOutputStream.close();
}
is.close();
}catch(Exception e){
Parameters.strExceptionMsg = e.getMessage();
}
}
}
STP10. \src\quanta\shopfloor\mainmenu\MyAdapter.java, 由於lvAppMenu需要展現圖片, 必須撰寫MyAdapter如下,
package quanta.shopfloor.mainmenu;
import java.util.List;
import java.util.Map;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
class MyAdapter extends SimpleAdapter
{
private int[] appTo;
private String[] appFrom;
private ViewBinder appViewBinder;
private List> appData;
private int appResource;
private LayoutInflater appInflater;
public MyAdapter(Context context, List> data, int resource, String[] from, int[] to)
{
super(context, data, resource, from, to);
appData = data;
appResource = resource;
appFrom = from;
appTo = to;
appInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public View getView(int position, View convertView, ViewGroup parent)
{
return createViewFromResource(position, convertView, parent, appResource);
}
private View createViewFromResource(int position, View convertView, ViewGroup parent, int resource)
{
View v;
if(convertView == null)
{
v = appInflater.inflate(resource, parent,false);
final int[] to = appTo;
final int count = to.length;
final View[] holder = new View[count];
for(int i = 0; i < count; i++)
{
holder[i] = v.findViewById(to[i]);
}
v.setTag(holder);
}else
{
v = convertView;
}
bindView(position, v);
return v;
}
private void bindView(int position, View view)
{
final Map dataSet = appData.get(position);
if(dataSet == null)
{
return;
}
final ViewBinder binder = appViewBinder;
final View[] holder = (View[])view.getTag();
final String[] from = appFrom;
final int[] to = appTo;
final int count = to.length;
for(int i = 0; i < count; i++)
{
final View v = holder[i];
if(v != null)
{
final Object data = dataSet.get(from[i]);
String text = data == null ? "":data.toString();
if(text == null)
{
text = "";
}
boolean bound = false;
if(binder != null)
{
bound = binder.setViewValue(v, data, text);
}
if(!bound)
{
/**
* 自定义适配器,关在在这里,根据传递过来的控件以及值的数据类型,
* 执行相应的方法,可以根据自己需要自行添加if语句。另外,CheckBox等
* 集成自TextView的控件也会被识别成TextView,这就需要判断值的类型
*/
if(v instanceof TextView)
{
//如果是TextView控件,则调用SimpleAdapter自带的方法,设置文本
setViewText((TextView)v, text);
}else if(v instanceof ImageView)
{
//如果是ImageView控件,调用自己写的方法,设置图片
setViewImage((ImageView)v, (Drawable)data);
}else
{
throw new IllegalStateException(v.getClass().getName() + " is not a " +
"view that can be bounds by this SimpleAdapter");
}
}
}
}
}
public void setViewImage(ImageView v, Drawable value)
{
v.setImageDrawable(value);
}
}
STP11. \res\layout\simple_list_item_2.xml, lvAppMenu的layout如下,
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<TableRow>
<ImageView
android:id="@+id/ivAppIcon"
android:layout_height="fill_parent"
android:layout_marginLeft="10dp"/>
<TableLayout
android:layout_height="wrap_content"
android:layout_weight="2" >
<TableRow>
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content" >
<TableRow>
<TextView
android:id="@+id/tvAppName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/tvPakName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="italic" />
</TableRow>
</TableLayout>
</TableRow>
<TableRow android:layout_width="fill_parent" >
<TableLayout
android:layout_height="wrap_content"
android:layout_weight="2" >
<TableRow>
<TextView
android:id="@+id/tvHttpAppVer"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_weight="1"
android:textStyle="italic" />
<TextView
android:id="@+id/tvPhoneAppVer"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textStyle="italic" />
</TableRow>
</TableLayout>
</TableRow>
</TableLayout>
<TextView
android:id="@+id/tvAppMsg"
android:layout_height="fill_parent"
android:layout_marginRight="10dp"
android:layout_marginTop="15dp"
android:textColor="#FF0000"
android:textStyle="bold|italic"/>
</TableRow>
</TableLayout>
結果如下,