安卓: 如何以编程方式安装 .apk 文件

387

我在Android下载二进制文件问题在Android上以编程方式安装应用程序的帮助下完成了这个功能。

我想一次实现自动更新和自动安装。这是本地的,所以它是非市场应用程序。

以下是我的代码:

public void Update(String apkurl){
    try {
        URL url = new URL(apkurl);
        HttpURLConnection c = (HttpURLConnection) url.openConnection();
        c.setRequestMethod("GET");
        c.setDoOutput(true);
        c.connect();

        String PATH = Environment.getExternalStorageDirectory() + "/download/";
        File file = new File(PATH);
        file.mkdirs();
        File outputFile = new File(file, "app.apk");
        FileOutputStream fos = new FileOutputStream(outputFile);

        InputStream is = c.getInputStream();

        byte[] buffer = new byte[1024];
        int len1 = 0;
        while ((len1 = is.read(buffer)) != -1) {
            fos.write(buffer, 0, len1);
        }
        fos.close();
        is.close();//till here, it works fine - .apk is download to my sdcard in download file

        Intent promptInstall = new Intent(Intent.ACTION_VIEW)
            .setData(Uri.parse(PATH+"app.apk"))
            .setType("application/android.com.app");
        startActivity(promptInstall);//installation is not working

    } catch (IOException e) {
        Toast.makeText(getApplicationContext(), "Update error!", Toast.LENGTH_LONG).show();
    }
}  

我的权限包括INTERNET, WRITE_EXTERNAL_STORAGE, INSTALL_PACKAGES以及DELETE_PACKAGES

当Intent promptInstall 被加载时,应用程序崩溃了 =/

那么,我是缺少权限还是我的代码不正确,或者有更好的方法来解决这个问题?


5
为了让应用程序正常工作,您应该移除安装和删除权限(INSTALL_PACKAGES和DELETE_PACKAGES),因为它们实际上不会被授予给您的应用程序,但如果拒绝时将产生令人困惑的警告日志。请注意,这并不改变原来的意思。 - Chris Stratton
2
我假设安装完成后,apk仍然在下载目录中。您如何检测安装成功并删除apk,以便不浪费空间? - Fraggle
2
只有在我删除了 c.setDoOutput(true); 时,它才对我起作用。请参见 http://stackoverflow.com/questions/12496789/file-not-found-exception-when-using-getinputstream - Stephen Hosking
我已经注释掉这行代码,然后它运行了,谢谢:c.setDoOutput(true); - Zied R.
这里有一个关于 Xamarin 的工作翻译 - https://dev59.com/p2445IYBdhLWcg3w_O8m#69774124 - Bondolin
显示剩余3条评论
4个回答

425

我解决了这个问题。我在setData(Uri)setType(String)中犯了错误。

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/download/" + "app.apk")), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

现在是正确的,我的自动更新正在工作。感谢帮助。=)

编辑 20.7.2016:

经过很长时间,我又不得不在另一个项目中使用这种更新方式。旧解决方案出现了许多问题,那段时间发生了很多变化,因此我必须用不同的方法来实现这个功能。以下是代码:

    //get destination to update file and set Uri
    //TODO: First I wanted to store my update .apk file on internal storage for my app but apparently android does not allow you to open and install
    //aplication with existing package from there. So for me, alternative solution is Download directory in external storage. If there is better
    //solution, please inform us in comment
    String destination = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/";
    String fileName = "AppName.apk";
    destination += fileName;
    final Uri uri = Uri.parse("file://" + destination);

    //Delete update file if exists
    File file = new File(destination);
    if (file.exists())
    //file.delete() - test this, I think sometimes it doesnt work
        file.delete();

    //get url of app on server
    String url = Main.this.getString(R.string.update_app_url);

    //set downloadmanager
    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
    request.setDescription(Main.this.getString(R.string.notification_description));
    request.setTitle(Main.this.getString(R.string.app_name));

    //set destination
    request.setDestinationUri(uri);

    // get download service and enqueue file
    final DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
    final long downloadId = manager.enqueue(request);

    //set BroadcastReceiver to install app when .apk is downloaded
    BroadcastReceiver onComplete = new BroadcastReceiver() {
        public void onReceive(Context ctxt, Intent intent) {
            Intent install = new Intent(Intent.ACTION_VIEW);
            install.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            install.setDataAndType(uri,
                    manager.getMimeTypeForDownloadedFile(downloadId));
            startActivity(install);

            unregisterReceiver(this);
            finish();
        }
    };
    //register receiver for when .apk download is compete
    registerReceiver(onComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));

33
有没有办法避免提示用户?我正在尝试通过测试套件自动更新一个应用程序,需要避免依赖用户来接受下载。 - Sam
6
如果没有外部存储器,怎样下载内部存储?下载完成后是否会删除apk文件? - Piraba
9
@TomBennett 不行,出于安全考虑,无法避免提示。只有当您的应用与ROM签名相同时,才能安装软件包- https://dev59.com/YWUo5IYBdhLWcg3w-zii#15660063 - Oleg Vaskevich
6
下载对我来说有效,但启动安装意图则不行。我使用了这个答案,然后使用以下内容触发安装:https://dev59.com/xlkT5IYBdhLWcg3wB7TP#40131196 - Alan
3
下载工作正常,但我遇到了一个错误,提示找不到处理意图的活动。将MIME类型硬编码为"application/vnd.android.package-archive"解决了这个问题。 - philcruz
显示剩余15条评论

73

为了ICS,我实现了你的代码并创建了一个继承AsyncTask的类。希望你能欣赏它!感谢你的代码和解决方案。

public class UpdateApp extends AsyncTask<String,Void,Void>{
    private Context context;
    public void setContext(Context contextf){
        context = contextf;
    }

    @Override
    protected Void doInBackground(String... arg0) {
        try {
            URL url = new URL(arg0[0]);
            HttpURLConnection c = (HttpURLConnection) url.openConnection();
            c.setRequestMethod("GET");
            c.setDoOutput(true);
            c.connect();

            String PATH = "/mnt/sdcard/Download/";
            File file = new File(PATH);
            file.mkdirs();
            File outputFile = new File(file, "update.apk");
            if(outputFile.exists()){
                outputFile.delete();
            }
            FileOutputStream fos = new FileOutputStream(outputFile);

            InputStream is = c.getInputStream();

            byte[] buffer = new byte[1024];
            int len1 = 0;
            while ((len1 = is.read(buffer)) != -1) {
                fos.write(buffer, 0, len1);
            }
            fos.close();
            is.close();

            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(Uri.fromFile(new File("/mnt/sdcard/Download/update.apk")), "application/vnd.android.package-archive");
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // without this flag android returned a intent error!
            context.startActivity(intent);


        } catch (Exception e) {
            Log.e("UpdateAPP", "Update error! " + e.getMessage());
        }
        return null;
    }
}   

要使用它,在您的主 Activity 中按照以下方式调用:

atualizaApp = new UpdateApp();
atualizaApp.setContext(getApplicationContext());
atualizaApp.execute("http://serverurl/appfile.apk");

3
下载的.apk文件没有问题,但是在从代码中安装时,我遇到了“解析软件包时出现问题”的错误。但是当我从模拟器中提取该.apk文件并手动安装时,一切都正常。您能告诉我问题出在哪里吗? - Big.Child
6
@Big.Child,你把apk下载到公开访问的文件夹了吗?我一开始把它下载到我的应用程序文件目录中,但是那样会导致我出现解析错误。现在我把它下载到 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) 这个位置,这对我来说可行。 - Jasper de Vries
3
请务必使用getExternalCacheDir().getAbsolutePath()。将文件保存到下载文件夹会在用户卸载应用程序时在设备上留下垃圾。相信我,没有人有意从Web上下载apk并希望将其保存在下载文件夹中。 - JonShipman
1
感谢您的答复,真的对我很有帮助。但是我遇到了java.io.FileNotFoundException的问题。问题出在这一行代码上:urlConnection.setDoOutput(true);。显然,这行代码会强制将 HTTP 协议中的 GET 请求变为 POST 请求,无论是否指定了 GET 方法。 - akelec
1
我正在使用相同的代码,只是使用内部存储路径不起作用。其他部分按照您的代码执行。 - Ankesh Roy
显示剩余8条评论

8
/*  
 *  Code Prepared by **Muhammad Mubashir**.
 *  Analyst Software Engineer.
    Email Id : muhammad.mubashir.bscs@gmail.com
    Skype Id : muhammad.mubashir.ansari
    Code: **August, 2011.**

    Description: **Get Updates(means New .Apk File) from IIS Server and Download it on Device SD Card,
                 and Uninstall Previous (means OLD .apk) and Install New One.
                 and also get Installed App Version Code & Version Name.**

    All Rights Reserved.
*/
package com.SelfInstall01;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import com.SelfInstall01.SelfInstall01Activity;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class SelfInstall01Activity extends Activity 
{
    class PInfo {
        private String appname = "";
        private String pname = "";
        private String versionName = "";
        private int versionCode = 0;
        //private Drawable icon;
        /*private void prettyPrint() {
            //Log.v(appname + "\t" + pname + "\t" + versionName + "\t" + versionCode);
        }*/
    }
    public int VersionCode;
    public String VersionName="";
    public String ApkName ;
    public String AppName ;
    public String BuildVersionPath="";
    public String urlpath ;
    public String PackageName;
    public String InstallAppPackageName;
    public String Text="";

    TextView tvApkStatus;
    Button btnCheckUpdates;
    TextView tvInstallVersion;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        //Text= "Old".toString();
        Text= "New".toString();


        ApkName = "SelfInstall01.apk";//"Test1.apk";// //"DownLoadOnSDcard_01.apk"; //      
        AppName = "SelfInstall01";//"Test1"; //

        BuildVersionPath = "http://10.0.2.2:82/Version.txt".toString();
        PackageName = "package:com.SelfInstall01".toString(); //"package:com.Test1".toString();
        urlpath = "http://10.0.2.2:82/"+ Text.toString()+"_Apk/" + ApkName.toString();

        tvApkStatus =(TextView)findViewById(R.id.tvApkStatus);
        tvApkStatus.setText(Text+" Apk Download.".toString());


        tvInstallVersion = (TextView)findViewById(R.id.tvInstallVersion);
        String temp = getInstallPackageVersionInfo(AppName.toString());
        tvInstallVersion.setText("" +temp.toString());

        btnCheckUpdates =(Button)findViewById(R.id.btnCheckUpdates);
        btnCheckUpdates.setOnClickListener(new OnClickListener() 
        {       
            @Override
            public void onClick(View arg0) 
            {
                GetVersionFromServer(BuildVersionPath); 

                if(checkInstalledApp(AppName.toString()) == true)
                {   
                    Toast.makeText(getApplicationContext(), "Application Found " + AppName.toString(), Toast.LENGTH_SHORT).show();


                }else{
                    Toast.makeText(getApplicationContext(), "Application Not Found. "+ AppName.toString(), Toast.LENGTH_SHORT).show();          
                }               
            }
        });

    }// On Create END.

    private Boolean checkInstalledApp(String appName){
        return getPackages(appName);    
    }

    // Get Information about Only Specific application which is Install on Device.
    public String getInstallPackageVersionInfo(String appName) 
    {
        String InstallVersion = "";     
        ArrayList<PInfo> apps = getInstalledApps(false); /* false = no system packages */
        final int max = apps.size();
        for (int i=0; i<max; i++) 
        {
            //apps.get(i).prettyPrint();        
            if(apps.get(i).appname.toString().equals(appName.toString()))
            {
                InstallVersion = "Install Version Code: "+ apps.get(i).versionCode+
                    " Version Name: "+ apps.get(i).versionName.toString();
                break;
            }
        }

        return InstallVersion.toString();
    }
    private Boolean getPackages(String appName) 
    {
        Boolean isInstalled = false;
        ArrayList<PInfo> apps = getInstalledApps(false); /* false = no system packages */
        final int max = apps.size();
        for (int i=0; i<max; i++) 
        {
            //apps.get(i).prettyPrint();

            if(apps.get(i).appname.toString().equals(appName.toString()))
            {
                /*if(apps.get(i).versionName.toString().contains(VersionName.toString()) == true &&
                        VersionCode == apps.get(i).versionCode)
                {
                    isInstalled = true;
                    Toast.makeText(getApplicationContext(),
                            "Code Match", Toast.LENGTH_SHORT).show(); 
                    openMyDialog();
                }*/
                if(VersionCode <= apps.get(i).versionCode)
                {
                    isInstalled = true;

                    /*Toast.makeText(getApplicationContext(),
                            "Install Code is Less.!", Toast.LENGTH_SHORT).show();*/

                    DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() 
                    {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            switch (which)
                            {
                            case DialogInterface.BUTTON_POSITIVE:
                                //Yes button clicked
                                //SelfInstall01Activity.this.finish(); Close The App.

                                DownloadOnSDcard();
                                InstallApplication();
                                UnInstallApplication(PackageName.toString());

                                break;

                            case DialogInterface.BUTTON_NEGATIVE:
                                //No button clicked

                                break;
                            }
                        }
                    };

                    AlertDialog.Builder builder = new AlertDialog.Builder(this);
                    builder.setMessage("New Apk Available..").setPositiveButton("Yes Proceed", dialogClickListener)
                        .setNegativeButton("No.", dialogClickListener).show();

                }    
                if(VersionCode > apps.get(i).versionCode)
                {
                    isInstalled = true;
                    /*Toast.makeText(getApplicationContext(),
                            "Install Code is better.!", Toast.LENGTH_SHORT).show();*/

                    DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() 
                    {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            switch (which)
                            {
                            case DialogInterface.BUTTON_POSITIVE:
                                //Yes button clicked
                                //SelfInstall01Activity.this.finish(); Close The App.

                                DownloadOnSDcard();
                                InstallApplication();
                                UnInstallApplication(PackageName.toString());

                                break;

                            case DialogInterface.BUTTON_NEGATIVE:
                                //No button clicked

                                break;
                            }
                        }
                    };

                    AlertDialog.Builder builder = new AlertDialog.Builder(this);
                    builder.setMessage("NO need to Install.").setPositiveButton("Install Forcely", dialogClickListener)
                        .setNegativeButton("Cancel.", dialogClickListener).show();              
                }
            }
        }

        return isInstalled;
    }
    private ArrayList<PInfo> getInstalledApps(boolean getSysPackages) 
    {       
        ArrayList<PInfo> res = new ArrayList<PInfo>();        
        List<PackageInfo> packs = getPackageManager().getInstalledPackages(0);

        for(int i=0;i<packs.size();i++) 
        {
            PackageInfo p = packs.get(i);
            if ((!getSysPackages) && (p.versionName == null)) {
                continue ;
            }
            PInfo newInfo = new PInfo();
            newInfo.appname = p.applicationInfo.loadLabel(getPackageManager()).toString();
            newInfo.pname = p.packageName;
            newInfo.versionName = p.versionName;
            newInfo.versionCode = p.versionCode;
            //newInfo.icon = p.applicationInfo.loadIcon(getPackageManager());
            res.add(newInfo);
        }
        return res; 
    }


    public void UnInstallApplication(String packageName)// Specific package Name Uninstall.
    {
        //Uri packageURI = Uri.parse("package:com.CheckInstallApp");
        Uri packageURI = Uri.parse(packageName.toString());
        Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI);
        startActivity(uninstallIntent); 
    }
    public void InstallApplication()
    {   
        Uri packageURI = Uri.parse(PackageName.toString());
        Intent intent = new Intent(android.content.Intent.ACTION_VIEW, packageURI);

//      Intent intent = new Intent(android.content.Intent.ACTION_VIEW);

        //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //intent.setFlags(Intent.ACTION_PACKAGE_REPLACED);

        //intent.setAction(Settings. ACTION_APPLICATION_SETTINGS);

        intent.setDataAndType
        (Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/download/"  + ApkName.toString())), 
        "application/vnd.android.package-archive");

        // Not open this Below Line Because...
        ////intent.setClass(this, Project02Activity.class); // This Line Call Activity Recursively its dangerous.

        startActivity(intent);  
    }
    public void GetVersionFromServer(String BuildVersionPath)
    {
        //this is the file you want to download from the remote server          
        //path ="http://10.0.2.2:82/Version.txt";
        //this is the name of the local file you will create
        // version.txt contain Version Code = 2; \n Version name = 2.1;             
        URL u;
        try {
            u = new URL(BuildVersionPath.toString());

            HttpURLConnection c = (HttpURLConnection) u.openConnection();           
            c.setRequestMethod("GET");
            c.setDoOutput(true);
            c.connect();

            //Toast.makeText(getApplicationContext(), "HttpURLConnection Complete.!", Toast.LENGTH_SHORT).show();  

            InputStream in = c.getInputStream();

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            byte[] buffer = new byte[1024]; //that stops the reading after 1024 chars..
            //in.read(buffer); //  Read from Buffer.
            //baos.write(buffer); // Write Into Buffer.

            int len1 = 0;
            while ( (len1 = in.read(buffer)) != -1 ) 
            {               
                baos.write(buffer,0, len1); // Write Into ByteArrayOutputStream Buffer.
            }

            String temp = "";     
            String s = baos.toString();// baos.toString(); contain Version Code = 2; \n Version name = 2.1;

            for (int i = 0; i < s.length(); i++)
            {               
                i = s.indexOf("=") + 1; 
                while (s.charAt(i) == ' ') // Skip Spaces
                {
                    i++; // Move to Next.
                }
                while (s.charAt(i) != ';'&& (s.charAt(i) >= '0' && s.charAt(i) <= '9' || s.charAt(i) == '.'))
                {
                    temp = temp.toString().concat(Character.toString(s.charAt(i))) ;
                    i++;
                }
                //
                s = s.substring(i); // Move to Next to Process.!
                temp = temp + " "; // Separate w.r.t Space Version Code and Version Name.
            }
            String[] fields = temp.split(" ");// Make Array for Version Code and Version Name.

            VersionCode = Integer.parseInt(fields[0].toString());// .ToString() Return String Value.
            VersionName = fields[1].toString();

            baos.close();
        }
        catch (MalformedURLException e) {
            Toast.makeText(getApplicationContext(), "Error." + e.getMessage(), Toast.LENGTH_SHORT).show();
            e.printStackTrace();
        } catch (IOException e) {           
            e.printStackTrace();
            Toast.makeText(getApplicationContext(), "Error." + e.getMessage(), Toast.LENGTH_SHORT).show();
        }
            //return true;
    }// Method End.

    // Download On My Mobile SDCard or Emulator.
    public void DownloadOnSDcard()
    {
        try{
            URL url = new URL(urlpath.toString()); // Your given URL.

            HttpURLConnection c = (HttpURLConnection) url.openConnection();
            c.setRequestMethod("GET");
            c.setDoOutput(true);
            c.connect(); // Connection Complete here.!

            //Toast.makeText(getApplicationContext(), "HttpURLConnection complete.", Toast.LENGTH_SHORT).show();

            String PATH = Environment.getExternalStorageDirectory() + "/download/";
            File file = new File(PATH); // PATH = /mnt/sdcard/download/
            if (!file.exists()) {
                file.mkdirs();
            }
            File outputFile = new File(file, ApkName.toString());           
            FileOutputStream fos = new FileOutputStream(outputFile);

            //      Toast.makeText(getApplicationContext(), "SD Card Path: " + outputFile.toString(), Toast.LENGTH_SHORT).show();

            InputStream is = c.getInputStream(); // Get from Server and Catch In Input Stream Object.

            byte[] buffer = new byte[1024];
            int len1 = 0;
            while ((len1 = is.read(buffer)) != -1) {
                fos.write(buffer, 0, len1); // Write In FileOutputStream.
            }
            fos.close();
            is.close();//till here, it works fine - .apk is download to my sdcard in download file.
            // So please Check in DDMS tab and Select your Emulator.

            //Toast.makeText(getApplicationContext(), "Download Complete on SD Card.!", Toast.LENGTH_SHORT).show();
            //download the APK to sdcard then fire the Intent.
        } 
        catch (IOException e) 
        {
            Toast.makeText(getApplicationContext(), "Error! " +
                    e.toString(), Toast.LENGTH_LONG).show();
        }           
    }
}

6
我的理解是:intent.setDataAndType(Uri.fromFile(new File(Environment.getExternalStorageDirectory() + "/download/" + ApkName.toString())), "application/vnd.android.package-archive"); startActivity(intent);这段代码的意思是设置Intent要操作的数据类型和数据源,其中数据源是通过文件路径获取的一个APK文件,并设置数据类型为Android应用程序包文件,最后执行Intent启动操作。 - Andras Balázs Lajtha
我怎么使用这个例子从Web服务下载apk? - Erum
点击按钮时,我收到了很多错误信息。 - Erum
是的,有太多错误了,你能修复并分享新代码吗? - Berkay Yıldız
1
如果这段代码更短、更简洁,那么它会更有帮助。一个好的起点是删除所有被注释掉的代码。 - Paul Wintz

5
感谢您分享这个内容。我已经实现并运行了它。然而:
1)我安装了应用程序的第一版(没有问题)
2)我将第二版放在服务器上。应用程序检索并保存到SD卡,并提示用户安装新的第二版包
3)第二版按预期安装和工作
4)问题是,每次启动应用程序时,它都要求用户重新安装第二版。
因此,我想解决方案就是简单地删除SD卡上的APK文件,但是异步任务会从服务器中检索第二个版本。
因此,阻止其尝试重新安装v2 apk的唯一方法是从SD卡和远程服务器中删除它。
正如您所想象的那样,这不太可行,因为我永远不知道所有用户何时收到最新版本。
非常感谢任何帮助解决此问题的帮助。
我已经实施了上面列出的“ldmuniz”方法。
更新:我正在考虑所有我的APK都被命名为相同的名称。我应该将myapk_v1.0xx.apk命名,并在该版本中主动设置远程路径以查找v.2.0,每当它发布时?
我测试了这个理论,它确实解决了这个问题。您需要给您的APK文件命名一些版本号,记得在当前发布的应用程序中始终设置下一个发布版本号。虽然不理想但是功能可行。

3
在开始更新过程之前,请与您的服务器确认是否有待处理的新更新。如果服务器返回Success,则开始更新过程(下载、保存和安装新更新),否则不采取任何行动。即,如果(存在任何新的可用更新){ "atualizaApp.execute("http://serverurl/appfile.apk");" } else { //不采取任何行动 } - Sarvan
未来前来此处者:检查已保存的apk版本、检查服务器上的版本以及检查已安装的版本,如果所有版本都相同,则无需担心。只有在服务器版本大于SD卡中保存的版本时才从服务器下载,并且只有在SD卡中保存的版本大于已安装的版本时才进行安装,这样可以帮助您。 - Tarcisio Wensing

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接