修改Android預設啟動項launcher
問題背景:
因為目前很多IPTV的廠商+廣電的要求,不允許使用者自己替換自己的launcher,為了保證利益,強行推廣自己的launcher,對播控平臺的掌控,於是就必須要求晶片原廠提供turnkey方案時符合其要求。下面用Android 5.1.1來舉例說明,理論上AN的版本對此模組改動差異不太多。
Google官方Android原生Launcher設定
Launcher是Android系統的桌面、是android系統的主要元件。android系統允許存在多個Launcher並設定預設主介面。
應用程式作為Home(主介面)需在Activity的intent-filter節點中新增如下內容
12 | <category android:name="android.intent.category.HOME" /><category android:name="android.intent.category.DEFAULT" /> |
當系統中存在多個Home app且沒有設定預設,使用者點選Home鍵會彈出如下圖所示的介面(圖一):
使用者可以選擇“只有一次”或者“總是”來啟動選擇的APP
一般情況下android中只會存在一個Home APP,系統啟動後會直接啟動此APP為預設,不需要使用者選擇。但是當系統中存在多個Home APP時,系統第一次啟動就會彈出上圖所示介面,讓使用者選擇其中一個APP作為主螢幕應用,如果使用者通過“始終”確認會設定選擇的APP為預設的Home,使用者通過“僅此一次”則此次以選擇的APP為Home,再次按home鍵還是會彈出選擇視窗.
針對廣電總局播控客製化方案
針對上圖,思考一下,可以採用兩種方案:
- 讓google原生態中的選框自動選中並且自動進入,這樣就符合我們的需求了
- 直接清除launcher選擇的堆疊,讓home的堆疊裡面永遠只保留我們需要的那個,這個是受Android原生code,AMS啟動home activity啟發
方案一
根據操作流程分析code flow,兩者互相糅合,理清問題的關鍵。
從上面彈出的選擇預設主介面的介面入手,通過追蹤發現上述介面是一個Activity,Activity程式碼路徑frameworks/base/core/java/com/android/internal/app/ResolverActivity.java
ResolverActivity分析
此Activity會獲取系統中所有的Home app,並根據系統的設定情況顯示如上介面。此類中有一個內部類ResolveListAdapter該類繼承自BaseAdapter,該類是Home app選擇介面的資料介面卡。
ResolveListAdapter會在ResolverActivity的onCreate方法中被初始化並會傳入一個ResolveInfo型別的List,ResolveListAdapter根據會傳入的List初始化一個List mList ,使用者的點選事件都會在ResolveListAdapter獲取資料。
使用者點選”ALWAYS”的事件發生在ResolverActivity的onButtonClick 方法中,此方法會獲取選中的Item的position、或者獲取使用者上一次啟動的Home app的,mAlwaysUseOption代表使用者選中的是否為歷史選擇(如2圖中的Launcher3),並呼叫startSelected。
123456789 | public void onButtonClick(View v) { final int id = v.getId(); startSelected(mAlwaysUseOption ? mListView.getCheckedItemPosition() : mAdapter.getFilteredPosition(), id == R.id.button_always, mAlwaysUseOption); dismiss();} |
StartSeletced中通過ResolveListAdapter獲取選擇的item代表的Home app。並且finish此activity
onIntentSelected會根據傳入的ResolveInfo設定預設的Home,並根據Intent跳轉到相應介面,onIntentSelected的程式碼在這裡就不列出。
123456789101112131415161718 | /** * 設定預設Home app並跳轉,結束此Activity * @param which 使用者選擇的Item的position * @param always 是否設定為總是 * @param filtered 是否非歷史選擇 */void startSelected(int which, boolean always, boolean filtered) { if (isFinishing()) { return; } ResolveInfo ri = mAdapter.resolveInfoForPosition(which, filtered); Intent intent = mAdapter.intentForPosition(which, filtered); //設定預設Home,並啟動 onIntentSelected(ri, intent, always); finish(); } |
ResolveListAdapter的相關程式碼
123456789101112131415161718192021 | /** * * @param position * @param filtered * @return */ public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { //mList為List<DisplayResolveInfo> return (filtered ? getItem(position) : mList.get(position)).ri; } /** * * @param position * @param filtered * @return */ public Intent intentForPosition(int position, boolean filtered) { //mList為List<DisplayResolveInfo> DisplayResolveInfo dri = filtered ? getItem(position) : mList.get(position); return intentForDisplayResolveInfo(dri); } |
解決措施
扯了半天了,分析完谷歌的原生流程,現在終於開始進行客製化的修改了。進入正題:
此Activity的onCreate方法中判斷是否為第一次啟動,如果是則呼叫startSelected方法設定預設Home app。
預設Home app的從ResolveListAdapter中獲取,所以在ResolveListAdapter中新增getDefaultHomePosition(String packageName)
方法,用於獲取預設home app在List 中的位置,程式碼如下:
1234567891011 | public int getDefaultHomePosition(String packageName){ for (int i = 0; i < mList.size(); i++) { ResolveInfo info = mList.get(i).ri; if (DEBUG) Log.w(TAG,"getDefaultHomePosition " + info.activityInfo.packageName); if (info.activityInfo.packageName.equals(packageName)) { return i; } } return -1; } |
在ResolverActivity中新增設定預設app的方法setupDefaultLauncher(),程式碼如下:
123456789101112131415161718192021222324252627282930313233 | //用於記錄預設home app是否設定過 private static final String DEFAULT_HOME = "persist.sys.default.home"; private void setupDefaultLauncher() { String first = ""; try{ first = SystemProperties.get(DEFAULT_HOME); }catch(Exception e){ Log.w(TAG,"exception error get DEFAULT_HOME"); } //判斷預設home 是否設定過,如果獲取的字串為空代表,未設定,否則return不在進行設定 if (!TextUtils.isEmpty(first)) { return; } //使用包名獲取所需設定的預設home app在ResolveListAdapter中的位置 int position = mAdapter.getDefaultHomePosition("home app包名"); //如果不存在則return if (position == -1) { if (DEBUG) Log.w(TAG,"not find default Home"); return; } //設定預設home app後,則新增記錄 try{ SystemProperties.set(DEFAULT_HOME,DEFAULT_HOME); }catch(Exception e){ Log.w(TAG,"exception error set DEFAULT_HOME"); } //設定預設home app,並跳轉 startSelected(position, true, true); //結束此activity dismiss(); } |
為了保證mAdapter被初始化 setupDefaultLauncher()的呼叫新增到ResolverActivity的onCreate函式中,程式碼如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344 | protected void onCreate(Bundle savedInstanceState, Intent intent, CharSequence title, int defaultTitleRes, Intent[] initialIntents, List<ResolveInfo> rList, boolean alwaysUseOption) { //其他初始化程式碼 ............ mIntent = new Intent(intent); mAdapter = new ResolveListAdapter(this, initialIntents, rList, mLaunchedFromUid, alwaysUseOption); //其它初始化程式碼 ............ if (mLaunchedFromUid < 0 || UserHandle.isIsolated(mLaunchedFromUid)) { // Gulp! finish(); return; } int count = mAdapter.mList.size(); //新增的程式碼 setupDefaultLauncher(); //原有邏輯 //如果系統中home app大於1 if (count > 1 || (count == 1 && mAdapter.getOtherProfile() != null)) { //初始化程式碼 ......... //如果home app等於1則設定唯一的home app為Home } else if (count == 1) { safelyStartActivity(mAdapter.intentForPosition(0, false)); mPackageMonitor.unregister(); mRegistered = false; finish(); return; } else { setContentView(R.layout.resolver_list); final TextView empty = (TextView) findViewById(R.id.empty); empty.setVisibility(View.VISIBLE); mListView = (ListView) findViewById(R.id.resolver_list); mListView.setVisibility(View.GONE); } //其它初始化程式碼 .......... } |
通過以上方法即可實現設定預設home的功能
方案二【優勢,可以通過長按home按鍵進行切換】
ActivityManagerService.java在每次啟動時執行,每次都預設啟動設定的launcher,當然,如果設定的launcher存在的話,設定其他的launcher為預設會無效,因為重新啟動時setDefaultLauncher()會將當前預設launcher清除。只有在解除安裝了設定預設啟動的launcher後才能設定其他launcher為預設啟動.
修改AMS:
1 | frameworks\base\services\java\com\android\server\am\ActivityManagerService.java |
新增客製化的method
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071 | private void setDefaultLauncher() { // get default component String packageName = "你指定的包名";//預設launcher包名 String className = "你指定的類名";////預設launcher入口 IPackageManager pm = ActivityThread.getPackageManager(); //判斷指定的launcher是否存在 if (hasApkInstalled(packageName)) { Slog.i(TAG, "defautl packageName = " + packageName + ", default className = " + className); //清除當前預設launcher ArrayList<IntentFilter> intentList = new ArrayList<IntentFilter>(); ArrayList<ComponentName> cnList = new ArrayList<ComponentName>(); mContext.getPackageManager().getPreferredActivities(intentList, cnList, null); IntentFilter dhIF = null; for (int i = 0; i < cnList.size(); i++) { dhIF = intentList.get(i); if (dhIF.hasAction(Intent.ACTION_MAIN) && dhIF.hasCategory(Intent.CATEGORY_HOME)) { mContext.getPackageManager().clearPackagePreferredActivities(cnList.get(i).getPackageName()); } } //獲取所有launcher activity Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); List<ResolveInfo> list = new ArrayList<ResolveInfo>(); try { list = pm.queryIntentActivities(intent, intent.resolveTypeIfNeeded(mContext.getContentResolver()), PackageManager.MATCH_DEFAULT_ONLY, getCurrentUserIdLocked()); } catch (RemoteException e) { throw new RuntimeException("Package manager has died", e); } // get all components and the best match IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_MAIN); filter.addCategory(Intent.CATEGORY_HOME); filter.addCategory(Intent.CATEGORY_DEFAULT); final int N = list.size(); //設定預設launcher ComponentName launcher = new ComponentName(packageName, className); ComponentName[] set = new ComponentName[N]; int defaultMatch = 0; for (int i = 0; i < N; i++) { ResolveInfo r = list.get(i); set[i] = new ComponentName(r.activityInfo.packageName, r.activityInfo.name); Slog.d(TAG, "r.activityInfo.packageName======= " + r.activityInfo.packageName); Slog.d(TAG, "r.activityInfo.name========= " + r.activityInfo.name); if (launcher.getClassName().equals(r.activityInfo.name)) { defaultMatch = r.match; } } try { pm.addPreferredActivity(filter, defaultMatch, set, launcher, getCurrentUserIdLocked()); } catch (RemoteException e) { throw new RuntimeException("Package manager has died", e); } } } private boolean hasApkInstalled(String packageName) { if (packageName == null || "".equals(packageName)) return false; android.content.pm.ApplicationInfo info = null; try { info = mContext.getPackageManager().getApplicationInfo(packageName, 0); return info != null; } catch (NameNotFoundException e) { return false; } } |
然後在ActivityManagerService類中的boolean startHomeActivityLocked()
方法第一行呼叫上面新增的setDefaultLauncher()
1234567891011121314151617181920212223242526272829303132333435363738 | boolean startHomeActivityLocked() { if (mFactoryTest == SystemServer.FACTORY_TEST_LOW_LEVEL && mTopAction == null) { // We are running in factory test mode, but unable to find // the factory test app, so just sit around displaying the // error message and don't try to start anything. return false; } ` //-------新增方法的執行位置--------------- setDefaultLauncher(); Intent intent = new Intent( mTopAction, mTopData != null ? Uri.parse(mTopData) : null); intent.setComponent(mTopComponent); if (mFactoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) { intent.addCategory(Intent.CATEGORY_HOME); } ActivityInfo aInfo = intent.resolveActivityInfo(mContext.getPackageManager(), STOCK_PM_FLAGS); if (aInfo != null) { intent.setComponent(new ComponentName( aInfo.applicationInfo.packageName, aInfo.name)); // Don't do this if the home app is currently being // instrumented. ProcessRecord app = getProcessRecordLocked(aInfo.processName, aInfo.applicationInfo.uid); if (app == null || app.instrumentationClass == null) { intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); mMainStack.startActivityLocked(null, intent, null, null, 0, aInfo, null, null, 0, 0, 0, false, false, null); } } return true; } |
新增後的方法全部內容如上,重新編譯android,燒錄,開機就能夠自動進入自定義的launcher
可以通過系統設定取消該launcher的預設設定,取消之後按home鍵會彈出launcher選擇提示框frameworks\base\core\java\com\android\internal\app\ResolverActivity.java
ResolverActivity類就是選擇開啟方式的彈出框
How to set default app launcher programmatically?
How to reset default launcher/home screen replacement?