首先準備一部已經Root的手機,然后打開Android Studio,下面我們開始快樂的寫代碼吧~
首先我們先分析具體的業務需求:
很簡單的一個需求,最主要的功能就是可以卸載App;
同時要求可以批量卸載;
既然能夠批量卸載,也就是說我們在UI交互上可以批量選擇;
能大量展示待卸載的App。
好的我們現在一步一步的來:首先我們先解決最主要的需求,卸載App!
有兩種方式可以實現App卸載:分為靜默方式和非靜默方式。
什么是靜默方式?意思就是說卸載完全是在系統后臺進行的,不需要用戶去點擊確認卸載。非靜默方式的意思顯而易見,卸載的時候需要用戶點擊確認,只有用戶確認卸載才會卸載。
我們先說非靜默方式卸載:
非靜默方式卸載的代碼如下;
public void unstallApp(String pageName){ Intent uninstallIntent = new Intent(); uninstallIntent.setAction(Intent.ACTION_DELETE); uninstallIntent.setData(Uri.parse("package:"+pageName)); startActivityForResult(uninstall_intent,1); }
從代碼中我們就可以看出來,這里開啟了一個活動,也就是所謂的應用卸載程序,然后把需要卸載的App包名交給它,它就會把這個App給卸載掉。這是正常的App卸載步驟。開啟這個應用卸載程序活動后,頁面就會跳轉到卸載頁面,然后等待用戶點擊確定或者取消,點擊確定就會執行卸載程序,點擊取消就會回退到原來的活動。在這里我們使用了startActivityForResult()方法來開啟應用卸載活動,目的是為了卸載完成后在回掉函數里面可以更新原來的App列表頁面。
非靜默方式代碼非常的簡單,也非常容易理解,但是這里有個不足之處,那就是如果我們一次性需要卸載十個APP應用,那么頁面將會跳轉十次,同時你也需要點擊十次確定!別忘了我們這里可是要求批量卸載,如果讓用戶去連續點擊十次確定,這樣會非常影響用戶體驗!所以非靜默方式卸載在這里使用并不是很好,靜默方式是更好的選擇!
靜默方式卸載:
靜默方式也就是意味著我們需要繞過安卓的界面,在后臺執行卸載命令,那么怎么做呢?很顯然,當然是使用命令了!使用命令的方式我們可以繞過安卓界面執行。
這里有兩種卸載App命令:
首先是adb命令:adbuninstall
還有一個pm命令:pm uninstall
我們可以看到這兩種命令寫法相同,命令的開頭不同,那么他們具體的差別在什么地方呢?應該用哪一種命令方式?還是兩種命令方式都合適呢?
我先不說區別,我們去實地的測試一下,首先我們先用adb命令去卸載。
代碼如下:
package com.example.uninstallapk; import android.util.Log; import java.io.DataOutputStream; /** * Created by 王將 on 2018/7/23. */ //adb命令翻譯執行類 public class RootCmd { /*** * @param command * @return */ public static boolean exusecmd(String command) { Process process = null; DataOutputStream os = null; try { process = Runtime.getRuntime().exec("su"); os = new DataOutputStream(process.getOutputStream()); os.writeBytes(command + "\n"); os.writeBytes("exit\n"); os.flush(); Log.e("updateFile", "======000==writeSuccess======"); process.waitFor(); } catch (Exception e) { Log.e("updateFile", "======111=writeError======" + e.toString()); return false; } finally { try { if (os != null) { os.close(); } if (process != null) { process.destroy(); } } catch (Exception e) { e.printStackTrace(); } } return true; } public static void unInstallApk(String pageName){ exusecmd("adb uninstall "+pageName); } }
主活動中我們調用:
RootCmd.unInstallApk("com.example.tset");
把想要卸載的App包名傳進去,運行一下,很快你就發現:整個應用崩潰了,出現了ANR問題,應用無反應。
好,我們改為pm命令試一下,結果發現成功了!
那么現在我們分析一下為什么adb命令會導致出現ANR問題,而pm命令就不會出現錯誤。
一個命令的下達,肯定會調用相應的方法去處理,只不過這個調用過程在系統的內部,我們外界是看不到的,只能得到命令執行的結果。就好比我們使用命令去卸載App應用,同樣也是在內部調用了卸載方法,那么具體這個方法是什么?在哪里呢?下面我們就去深入的探討一下。
Android系統卸載App應用都是調用了一個類 中方法,不管是非靜默模式還是靜默模式,這個類就是PackageInstaller類。當然Android系統安裝App也同樣是調用的它里面的方法,這個類功能從它的名字上就可以看出來:打包安裝程序。
當然這個類我們在平常的開發中是用不到的,同樣也是無法調用的,這個類同樣也是一個底層調用的類。在這個類中我們可以找到具體的卸載App方法,讓我們看一下源碼:
/** * Uninstall the given package, removing it completely from the device. This * method is only available to the current "installer of record" for the * package. * * @param packageName The package to uninstall. * @param statusReceiver Where to deliver the result. */ public void uninstall(@NonNull String packageName, @NonNull IntentSender statusReceiver) { uninstall(packageName, 0 /*flags*/, statusReceiver); } /** * Uninstall the given package, removing it completely from the device. This * method is only available to the current "installer of record" for the * package. * * @param packageName The package to uninstall. * @param flags Flags for uninstall. * @param statusReceiver Where to deliver the result. * * @hide */ public void uninstall(@NonNull String packageName, @DeleteFlags int flags, @NonNull IntentSender statusReceiver) { uninstall(new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), flags, statusReceiver); } /** * Uninstall the given package with a specific version code, removing it * completely from the device. This method is only available to the current * "installer of record" for the package. If the version code of the package * does not match the one passed in the versioned package argument this * method is a no-op. Use {@link PackageManager#VERSION_CODE_HIGHEST} to * uninstall the latest version of the package. * * @param versionedPackage The versioned package to uninstall. * @param statusReceiver Where to deliver the result. */ public void uninstall(@NonNull VersionedPackage versionedPackage, @NonNull IntentSender statusReceiver) { uninstall(versionedPackage, 0 /*flags*/, statusReceiver); } /** * Uninstall the given package with a specific version code, removing it * completely from the device. This method is only available to the current * "installer of record" for the package. If the version code of the package * does not match the one passed in the versioned package argument this * method is a no-op. Use {@link PackageManager#VERSION_CODE_HIGHEST} to * uninstall the latest version of the package. * * @param versionedPackage The versioned package to uninstall. * @param flags Flags for uninstall. * @param statusReceiver Where to deliver the result. * * @hide */ @RequiresPermission(anyOf = { Manifest.permission.DELETE_PACKAGES, Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull VersionedPackage versionedPackage, @DeleteFlags int flags, @NonNull IntentSender statusReceiver) { Preconditions.checkNotNull(versionedPackage, "versionedPackage cannot be null"); try { mInstaller.uninstall(versionedPackage, mInstallerPackageName, flags, statusReceiver, mUserId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }
這個是PackageInstaller類中的四個uninstall()方法,具體的功能就是卸載App應用。
當然這四個方法用于卸載不同狀態的應用,具體的使用請看官方給出的描述文檔,這里不再具體的做出分析。
現在我們知道了卸載App調用的是PackageInstaller類的uninstall()方法,那么這個和命令的方式有什么關系呢?我們看一下PackageInstaller類的所處路徑你就明白了,PackageInstaller類的所處路徑為/android/content/pm/PackageInstaller.java,具體在博主這里的完整路徑為:
很明顯,在/pm路徑下。pm全稱package manager,意思包的管理者,pm命令說白了就是包管理命令,進一步說,只有使用pm命令才會調用/pm路徑下的底層方法,也就是說才會執行包文件的操作。這下你明白為什么使用adb會導致ANR問題了吧,因為程序找不到執行方法!
好了,現在我們解決了最重要的需求,靜默卸載App,那么接下來的需求就很簡單實現了,批量卸載,批量選擇,這里直接使用一個循環不停的執行卸載命令就好了。按照這個思路我們開始寫代碼。
首先是界面UI部分:
使用ScrollView嵌套一個LinearLayout布局來實現App列表,其中單個的App信息使用動態加載的形式添加。
下面是一個App信息子布局:
很簡單,兩個控件組成,一個ChexBox控件提供勾選,一個TextView用來展示App的標簽。
接下來我們就需要寫主活動中的邏輯性操作了:
首先貼上我們的MainActivity代碼:
public class MainActivity extends AppCompatActivity { LinearLayout linearLayout; Listpages=new ArrayList<>(); List views=new ArrayList<>(); ProgressDialog progressDialog; List packageInfos=new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); linearLayout = (LinearLayout) findViewById(R.id.linear1); Button button=(Button) findViewById(R.id.start_delete); PackageManager packageManager=getPackageManager(); packageInfos=packageManager.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES); int id=0; for (PackageInfo packageInfo:packageInfos){ String str=packageInfo.applicationInfo.loadLabel(getPackageManager()).toString(); linearLayout.addView(getChoiceView(linearLayout,str,id)); id++; } button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new DEleteApk().execute(); } }); } private View getChoiceView(LinearLayout root, final String pageName, int id){ final View view = LayoutInflater.from(this).inflate(R.layout.choice_layout, root, false); final CheckBox checkBox=(CheckBox) view.findViewById(R.id.page_id); final TextView textView=(TextView) view.findViewById(R.id.page_name); view.setTag(id); checkBox.setTag(view); checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked){ views.add((View) checkBox.getTag()); pages.add((int) view.getTag()); }else { View view1=(View) checkBox.getTag(); views.remove(view1); pages.remove(getIndexPages((int) view1.getTag())); } } }); textView.setText(pageName); return view; } public int getIndexPages(int id){ int index=0; int j=0; for (int i:pages){ if (i==id){ index=j; break; } j++; } return index; } class DEleteApk extends AsyncTask { @Override protected void onPreExecute() { progressDialog=new ProgressDialog(MainActivity.this); progressDialog.setTitle("正在卸載中"); progressDialog.setMessage("請稍后..."); progressDialog.setCancelable(true); progressDialog.show(); } @Override protected Object doInBackground(Object[] objects) { for (int id:pages){ RootCmd.unInstallApk(packageInfos.get(id).packageName); } return true; } @Override protected void onPostExecute(Object o) { progressDialog.dismiss(); pages.removeAll(pages); for (View view:views){ linearLayout.removeView(view); } views.removeAll(views); } } }
在代碼中有兩部分講解一下:
首先是getChoiceView()方法。在這個方法中我們主要獲取用戶勾選的App是哪些。當用戶點擊勾選的時候,我們就把對應App的下標值給存下來,同時存下來的還有相應的子View,存放子View的目的是為了在卸載完成之后更新我們的App列表。在用戶點擊取消勾選,我們還需要把之前存放的相關信息給移除掉,確保卸載的都是用戶最終確定刪除的。
存好了相應的信息,下面就是執行pm命令部分。在這里我使用線程來開啟pm命令,可以很清楚地看到,在這里我使用了AsyncTask 框架。在線程開啟前,也就是pm命令開始之前,我們彈出一個ProgressDialog,目的就是告訴用戶正在卸載請稍等,因為pm命令執行起來到結束會需要一定的時間;然后就開始執行pm命令,使用循環挨著挨卸載List中用戶選定的App,執行結束后關閉ProgressDialog,然后清空我們的Liset,同時還要更改我們的UI界面。
這里選用AsyncTask 框架有一個好處,那就是可以明確的知道命令執行結束的時間,在命令結束之后更改UI。如果不使用AsyncTask 框架,那么就比較難以掌握pm命令執行結束的時候,畢竟這個也沒有什么相關的回掉函數,在結束后UI處理上難以下手。使用AsyncTask 框架后,就不需要擔心這個問題,執行結束后自然會執行收尾工作,這樣更新IUI就方便多了。
好了,本篇文章到此結束。有需要引用的請標明出處,謝謝!