[Andriod] 透過HTTP存取JSON實踐APPMainMenu+自動更新

  • 3227
  • 0
  • 2013-05-13

原先想使用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>

 

結果如下,