Android 自动检测版本并升级

如何在应用启动时检查版本更新对应用进行升级,解决的方案有多种。本文将阐述利用Android 自带的DownloadManager进行下载apk文件,已经下载成功后自动安装。DownloadManager在后台下载文件时会有进度条的,所以省去了开发进度条的麻烦。

思路:在AndroidManifest.xml文件中定义了versionCode以及versionName,称为旧的,如果使用AndroidStudio开发的话,这些东西都移到了build.gradle文件中。而在服务器端,也要配置(通常在配置文件中)一个版本号以及对应的版本名称,称为最新的,此外还必须配置一个apk文件的下载路径Url,当然还可以配置一些升级或更新的内容。然后在应用启动时,从服务器获取该最新的信息,可以用一个对象封装这些信息。然后对比新旧版本号,若新版本号大于旧版本号,则弹出对话框让用户进行确认升级,确认后用DownloadMananger进行下载获取的Url。DownloadManager在下载完成后会发一个通知给android系统,所以需要定义一个BroadcastReceiver来接收该通知,在onReceive()方法中将该apk进行安装。这样就实现了该功能。

有些设备是没有SD卡的,所以需要妥善设置apk的下载存放位置。

1.为了完成上述工作,定义如下一个辅助类.

ApplicationHelper.java

package com.jykj.departure.util;

import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
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.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Environment;
import android.provider.Settings;
import android.telephony.TelephonyManager;

import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

public class ApplicationHelper {
    public static final CharSequence TOAST_NO_NET = "很遗憾,木有网络";

    //判断某个文件是否为apk文件
    public static boolean isApkFile(String fileName){
        fileName = fileName.toLowerCase();
        int idx = fileName.lastIndexOf(".apk");
        return  idx >=0;
    }
    /**
     * 获得AndroidManifest.xml中的versionCode
     * 
     * @param context 上下文
     * @return int
     */
    public static int getLocalVersionCode(Context context) {
        try {
            PackageInfo pi = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), 0);
            return pi.versionCode;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return 0;
    }

    /**
     * 获得AndroidManifest.xml中的versionName
     * 
     * @param context 上下文
     * @return str
     */
    public static String getLocalVersionName(Context context) {
        try {
            PackageInfo pi = context.getPackageManager().getPackageInfo(
                    context.getPackageName(), 0);
            return pi.versionName;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
    /**
     * 安装apk文件
     *
     * @param context 上下文
     * @param uri uri
     */
    public static void installApk(Context context, Uri uri) {
        Intent installIntent = new Intent(Intent.ACTION_VIEW);
        installIntent.setDataAndType(uri,
                "application/vnd.android.package-archive");
        installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(installIntent);
    }

    /**
     * 卸载apk程序
     *
     * @param context 上下文
     * @param uri uri
     */
    public static void uninstallApk(Context context, Uri uri, String packageName) {
        Uri packageURI = Uri.parse("package:" + packageName);
        Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI);
        context.startActivity(uninstallIntent);

    }

    /**
     * 判断是否有网络连接
     * 
     * @param context 上下文
     * @return bool
     */
    public static boolean isNetworkConnected(Context context) {
        if (context != null) {
            ConnectivityManager mConnectivityManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo mNetworkInfo = mConnectivityManager
                    .getActiveNetworkInfo();
            if (mNetworkInfo != null && mNetworkInfo.isConnected()
                    && mNetworkInfo.getState() == NetworkInfo.State.CONNECTED) {
                return mNetworkInfo.isAvailable();
            }
        }
        return false;
    }
}

2.从服务器获取的信息的封装类

Apkinfo.java

package com.sudoku.jack.sudoku.model;

import java.io.Serializable;

public class Apkinfo implements Serializable{
    private String versionName;
    private int versionCode;
    private String apkName;
    private String upgradeContent;
    private String apkURL;
    public String getVersionName() {
        return versionName;
    }
    public void setVersionName(String versionName) {
        this.versionName = versionName;
    }
    public int getVersionCode() {
        return versionCode;
    }
    public void setVersionCode(int versionCode) {
        this.versionCode = versionCode;
    }

    /**
     * @return 如 Express-v1.0.3.apk
     */
    public String getApkName() {
        return apkName;
    }
    public void setApkName(String apkName) {
        this.apkName = apkName;
    }
    public String getUpgradeContent() {
        return upgradeContent;
    }
    public void setUpgradeContent(String upgradeContent) {
        this.upgradeContent = upgradeContent;
    }

    /**
     * @return apk下载地址URL
     */
    public String getApkURL() {
        return apkURL;
    }
    public void setApkURL(String apkURL) {
        this.apkURL = apkURL;
    }

}

3.将服务器端的信息JSON格式解析为ApkInfo对象

ParseJSON.java

import com.sudoku.jack.sudoku.model.Apkinfo;
import org.json.JSONException;
import org.json.JSONObject;

public class ParseJSON {
    public static final String RetCode = "RetCode";
    public static final String RetMsg = "RetMsg";
    public static final String ResultObj = "ResultObj";
    // 状态码
    public static final String CODE_SUCCESS = "0000";
    public static Apkinfo parseToApkinfo(@NonNull JSONObject jo) throws JSONException {
        String url = jo.getString("apkURL");
        if(url==null||url.isEmpty()){
            return  null;
        }
        Apkinfo a = new Apkinfo();
        a.setVersionCode(jo.getInt("apkVer"));
        a.setApkURL(url);
        Log.e("TAG","ParseToApkInfo version code:"+ a.getVersionCode());
        if(!jo.isNull("apkUpgradeContent")) a.setUpgradeContent(jo.getString("apkUpgradeContent"));
        else a.setUpgradeContent("请务必更新以确保功能使用稳定!");
        // 解析apkURL
        int index = url.lastIndexOf("/");
        String name = url.substring(index + 1);
        Log.e("TAG","ParseToApkInfo URL:" + url);
        a.setApkName(name);// Express-v1.0.3.apk
        int idxStart = name.lastIndexOf("v");
        if(idxStart == -1) idxStart = name.lastIndexOf("V");
        if(idxStart ==-1) idxStart = name.lastIndexOf("-");
        if(idxStart ==-1) idxStart = name.lastIndexOf("_");
        int idxEnd = name.lastIndexOf(".apk");
        if(idxEnd==-1) idxEnd = name.lastIndexOf(".");
        try{
            String version = name.substring(idxStart+1, idxEnd);
            Log.e("TAG","ParseToApkInfo version:"+ version);
            a.setVersionName(version);
        }catch (Exception e){
            e.printStackTrace();
        }
        return a;
    }
}

4.从服务器获取信息的任务类

GetNewestVersionTask.java

package com.sudoku.jack.sudoku.task;

import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.widget.Toast;

import com.sudoku.jack.sudoku.model.Apkinfo;
import com.sudoku.jack.sudoku.util.Helper;
import com.sudoku.jack.sudoku.util.HttpHelper;
import com.sudoku.jack.sudoku.util.ParseJSON;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * 获取最新版本的apk信息 的AsyncTask
 *
 * @author Administrator
 */
public class GetNewestVersionTask extends AsyncTask {
    private ProgressDialog pd;
    private Context mContext;
    private boolean mShowProgress;
    private String exceptionInfo;//后台任务处理出现的异常信息

    /**
     * 构造方法:获取最新版本的apk信息 的AsyncTask
     *
     * @param context      启动任务的activity
     * @param showProgress 是否显示进度条
     */
    public GetNewestVersionTask(Context context, boolean showProgress) {
        mContext = context;
        mShowProgress = showProgress;
    }

    @Override
    protected void onPreExecute() {
        pd = new ProgressDialog(mContext);
        if (mShowProgress) {
            pd.setProgressStyle(ProgressDialog.STYLE_SPINNER);
            pd.setTitle("检查更新");
            pd.setMessage("获取数据中,请稍后...");
            pd.show();
        }
    }

    @Override
    protected String doInBackground(Void... params) {
        /*
        String inputParams = WebServiceHelper.getInputParam(WebServiceHelper
                .getNewestVersionParams(Helper.getIMEI(mContext)));
        try {
            return WebServiceHelper.getService(inputParams, WebServiceHelper.S_getNewestVersion);
        } catch (IOException | XmlPullParserException e) {
            e.printStackTrace();
            exceptionInfo = "获取最新版本时出现异常:" + e.getMessage();
            return null;
        }
        */
        // 以下是用来测试的,实际使用时应使用上面的代码来获取信息,可以是http请求或者Web Service
        String url = "http://ftp.daumkakao.com/eclipse/tools/pdt/downloads/pdt-Update-3.6.0.201509151953.zip";
        JSONObject obj = new JSONObject();
        try {
            obj.put("RetCode", "0000");
            obj.put("RetMsg", "");
            JSONObject apk = new JSONObject();
            apk.put("apkVer", 2);
            apk.put("apkURL", url);
            obj.put("ResultObj", apk);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return obj.toString();
    }

    @Override
    protected void onPostExecute(String result) {
        if (mShowProgress&&pd!=null) pd.cancel();
        if (result == null) {
            Toast.makeText(mContext, exceptionInfo, Toast.LENGTH_SHORT).show();
            return;
        }
        try {
            JSONObject obj = new JSONObject(result);
            if (ParseJSON.RESULT_SUCCESS.equals(obj.getString(ParseJSON.RetCode))) {
                JSONObject jo = obj.getJSONObject(ParseJSON.ResultObj);
                final Apkinfo apkinfo = ParseJSON.parseToApkinfo(jo);
                //CacheHelper.setCacheApkinfo(apkinfo);
                if(apkinfo==null ||  ApplicationHelper.getLocalVersionCode(mContext)>=apkinfo.getVersionCode()) {
                    if (mShowProgress) {
                        Toast.makeText(mContext, "已经是最新版本!", Toast.LENGTH_SHORT).show();
                    }
                }else{
                    String message = "版本升级:"+ ApplicationHelper.getLocalVersionName(mContext) + "->" + apkinfo.getVersionName() + "\n";
                    message += apkinfo.getUpgradeContent();
                    new AlertDialog.Builder(mContext).setTitle("检测到新版本").
                            setMessage(message).setPositiveButton("立即更新", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            //检查是否已经下载过,直接安装
                            Uri uri = ApplicationHelper.checkFileExist(apkinfo.getApkName());
                            if (uri != null) {
                                if(ApplicationHelper.isApkFile(apkinfo.getApkName())) ApplicationHelper.installApk(mContext, uri);
                            } else {
                                HttpHelper.download(mContext, apkinfo.getApkName(), Uri.parse(apkinfo.getApkURL()));
                            }
                        }
                    }).setNegativeButton("下次再说", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                        }
                    }).create().show();
                }
            } else {
                Toast.makeText(mContext, obj.getString(ParseJSON.RetMsg), Toast.LENGTH_SHORT).show();
            }
        } catch (JSONException e) {
            e.printStackTrace();
            Toast.makeText(mContext, e.getMessage(), Toast.LENGTH_SHORT).show();
        }
    }
}

5.下载完成后需要一个广播接收器,用来自动安装文件

DownloadCompleteReceiver.java

import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;

import com.jykj.departure.util.ApplicationHelper;

import java.io.File;


/**
 * DownloadManager下载完后 ,DOWNLOAD_SERVICE 会发送广播提示下载完成
 */
public class DownloadCompleteReceiver extends BroadcastReceiver {

    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
            /**
             * The download manager is a system service that handles
             * long-running HTTP downloads.
             */
            DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);// 从下载服务获取下载管理器

            DownloadManager.Query query = new DownloadManager.Query();
            long downid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID,-1);
            //downloadManager.getUriForDownloadedFile(downid);
            query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);// 设置过滤状态:成功
            query.setFilterById(downid);
            Cursor c = downloadManager.query(query);// 查询以前下载过的‘成功文件’
            if (c!=null&&c.moveToFirst()) {// 移动到最新下载的文件
                String fileUri = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                Log.e("TAG","COLUMN_LOCAL_URI:"+fileUri);
                int fileNameIdx = c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
                String fileName = c.getString(fileNameIdx);
                File file  = new File(fileName);
                Log.e("TAG", "======文件名称=====:" + fileName);
                if(ApplicationHelper.isApkFile(fileName)) ApplicationHelper.installApk(context, Uri.parse("file://"+fileName));
                c.close();
            }
            // 安装
        }
    }
}

6.最后别忘了在AndroidManifest.xml文件中加上如下配置

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 <receiver
            android:name=".receiver.DownloadBroadcastReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
            intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED" />
            intent-filter>
        receiver>

注意:android:exported=”true” ,如果设置为false,该接收器将接收不到广播。
两个权限也必不可少。

7.利用DownloadManager下载apk文件

HttpHelper.java

package com.jykj.departure.util;

import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Map.Entry;

/**
 *  通用的与Http相关的辅助类
 */
public class HttpHelper {
    private static String JSESSIONID; //定义一个静态的字段,保存sessionID 例如JSESSIONID=AD5F5C9EEB16C71EC3725DBF209F6178
    /**
     * 利用android 下载管理器 下载文件
     * 
     * @param context 上下文
     * @param fileName 文件名
     * @param uri
     *            http或https
     * @return 如果已经下载过返回-1,否则返回Download ID
     */
    public static long download(Context context, String fileName, Uri uri) {
        Toast.makeText(context, "已经转入后台下载!", Toast.LENGTH_SHORT).show();
        DownloadManager manager = (DownloadManager) context
                .getSystemService(Context.DOWNLOAD_SERVICE); // 初始化下载管理器
        Request request = new Request(uri);// 创建请求
        request.setAllowedNetworkTypes(Request.NETWORK_MOBILE
                | Request.NETWORK_WIFI);// 设置允许使用的网络类型,这里是移动网络和wifi都可以
        request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        request.setAllowedOverRoaming(false);// 漫游
        /*request.setDestinationInExternalPublicDir(
            Environment.DIRECTORY_DOWNLOADS, fileName);*/
        //判断是否有SD卡,如果有设置路径,没有则使用默认路径
        if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
            request.setDestinationInExternalFilesDir(context,
                Environment.DIRECTORY_DOWNLOADS, fileName);
      /*    File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),fileName);
        request.setDestinationUri(Uri.fromFile(file));*/
        return manager.enqueue(request);// 将下载请求放入队列
    }

    /**
     * http POST 请求
     * 
     * @param urlString
     *            http请求
     * @param content
     *            请求正文,正文内容其实跟get的URL中'?'后的参数字符串一致
     * @return str
     * @throws IOException IOException
     */
    public static String requestPost(String urlString, String content)
            throws IOException {
        System.out.println("session:"+JSESSIONID+",URL:"+urlString+"?"+content);
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(6 * 1000);
        // Read from the connection. Default is true.
        conn.setDoInput(true);
        // Output to the connection. Default is
        // false, set to true because post
        // method must write something to the
        // connection
        // 设置是否向connection输出,因为这个是post请求,参数要放在
        // http正文内,因此需要设为true
        conn.setDoOutput(true);
        // Post cannot use caches
        conn.setUseCaches(false);
        // Set the post method. Default is GET
        conn.setRequestMethod("POST");
        // This method takes effects to every instances of this class. URLConnection.setFollowRedirects是static函数,作用于所有的URLConnection对象。
        // connection.setFollowRedirects(true);
        // This methods only takes effacts to this instance.
        conn.setInstanceFollowRedirects(true);
        // Set the content type to urlencoded,because we will write some URL-encoded content to the
        // connection. Settings above must be set before connect!
        // 配置本次连接的Content-type,配置为application/x-www-form-urlencoded的
        // 意思是正文是urlencoded编码过的form参数,下面我们可以看到我们对正文内容使用URLEncoder.encode
        // 进行编码
        conn.setRequestProperty("Content-Type",
                "application/x-www-form-urlencoded");
        if(null != JSESSIONID){
            conn.setRequestProperty("Cookie",JSESSIONID);
        }
        // 连接,从postUrl.openConnection()至此的配置必须要在connect之前完成,
        // 要注意的是connection.getOutputStream会隐含的进行connect。
        conn.connect();

        DataOutputStream out = new DataOutputStream(conn.getOutputStream());
        // The URL-encoded contend DataOutputStream.writeBytes将字符串中的16位的unicode字符以8位的字符形式写道流里面
        out.writeBytes(content);
        out.flush();
        out.close(); // flush and close
        //System.out.println(conn.getResponseMessage());
        BufferedReader reader = new BufferedReader(new InputStreamReader(
                conn.getInputStream(), "utf-8"));// 设置编码,否则中文乱码
        String line;
        StringBuilder sb = new StringBuilder();
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        String cookieval = conn.getHeaderField("set-cookie");
        System.out.println("set-cookie:"+cookieval);
        if(cookieval != null) {
            JSESSIONID = cookieval.substring(0, cookieval.indexOf(";"));
        }
        reader.close();
        conn.disconnect();
        return sb.toString();
    }

    /**
     * http Post请求 ,参考 {@link #requestPost(String,String) requestPost(String,String)}
     * 
     * @param urlString
     *            请求url
     * @param map
     *            封装了正文内容的map
     * @return str
     * @throws IOException IOException
     * 
     */
    public static String requestPost(String urlString, Map map)
            throws IOException {
        if(map==null) return requestPost(urlString);
        // 正文,正文内容其实跟get的URL中'?'后的参数字符串一致
        // String content =
        // "[email protected]"
        // + "&activatecode=" + URLEncoder.encode("中国聚龙", "utf-8");
        String content = "";
        for (Entry en : map.entrySet()) {
            String key = en.getKey();
            String value = URLEncoder.encode(en.getValue(), "utf-8");
            if (!content.isEmpty()) {
                content += "&";
            }
            content += key + "=" + value;
        }
        return requestPost(urlString, content);
    }

    /**
     * http Post请求,不带content ,参考 {@link #requestPost(String,String) requestPost(String,String)}
     * @param urlString   请求url
     * @return str
     * @throws IOException IOException
     */
    public static String requestPost(String urlString)
            throws IOException {
        return requestPost(urlString, "");
    }
    /**
     * http GET 请求
     * 
     * @param url url
     * @return str
     * @throws IOException IOException
     */
    public static String requestGet(String url) throws IOException {
        URL getUrl = new URL(url);
        // 根据拼凑的URL,打开连接,URL.openConnection函数会根据URL的类型,
        // 返回不同的URLConnection子类的对象,这里URL是一个http,因此实际返回的是HttpURLConnection
        HttpURLConnection connection = (HttpURLConnection) getUrl
                .openConnection();
        connection.setConnectTimeout(6 * 1000);
        // 进行连接,但是实际上get request要在下一句的connection.getInputStream()函数中才会真正发到
        // 服务器
        if(null != JSESSIONID){
            connection.setRequestProperty("Cookie", JSESSIONID);
        }
        connection.connect();
        // 取得输入流,并使用Reader读取
        BufferedReader reader = new BufferedReader(new InputStreamReader(
                connection.getInputStream(), "utf-8"));// 设置编码,否则中文乱码
        String line;
        StringBuilder sb = new StringBuilder();
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        String cookieval = connection.getHeaderField("set-cookie");
        System.out.println("set-cookie:"+cookieval);
        if(cookieval != null) {
            JSESSIONID = cookieval.substring(0, cookieval.indexOf(";"));
        }
        reader.close();
        // 断开连接
        connection.disconnect();
        return sb.toString();
    }
    /**
     * 读取图片
     * @param url 图片url
     * @return Bitmap
     * @throws IOException IOException
     */
    public static Bitmap getBitmap(String url) throws IOException {
        // 获得连接
        HttpURLConnection conn = (HttpURLConnection) new URL(url)
                .openConnection();
        // 设置超时时间为6000毫秒,conn.setConnectionTiem(0);表示没有时间限制
        conn.setConnectTimeout(6000);
        // 连接设置获得数据流
        conn.setDoInput(true);
        // 不使用缓存
        conn.setUseCaches(false);
        // 这句可有可无,没有影响
        conn.connect();
        // 得到数据流
        InputStream is = conn.getInputStream();
        // 解析得到图片
        Bitmap bitMap = BitmapFactory.decodeStream(is);
        is.close();
        return bitMap;
    }
}

8.使用

在登录的Activity或者主Activity的onCreate()方法中进行调用,如

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
         //检查版本更新
        new GetNewestVersionTask(this,false).execute();
    }

这么一个小小的功能却要如此折腾。。。

*309. 《道德经》第六十九章2 悟道之人有三宝
原文:我恒有三宝,持而保之:一曰慈,二曰俭,三曰不敢为天下先。夫慈,故能勇;俭,故能广;不敢为天下先,故能为成事长。
译文:悟道之人一直有三个宝贝,好好地拿在手里:第一是利他,第二是降低自己的欲望,第三是不能把自己的利益放在天下人的前面。因为心怀慈悲,所以做事勇敢;因为对自己节俭,所以机会广;不敢把自己的利益放在前面,所以能被推举为做事的首领。

先天下之忧而忧,后天下之乐而乐,这是真正做事的人的品格。

你可能感兴趣的:(Android)