GMS介紹
GMS全稱為GoogleMobile Service,即谷歌移動服務。GMS是Google開發(fā)并推動Android的動力,是谷歌程序運行的基礎。
GMS提供有GooglePlay、Search、Google語音、Gmail、Contact Sync、Calendar Sync、Talk、Google Maps、Street View、YouTube、Android Market等服務,GMS為安卓上的谷歌公司系列應用提供支持
GMS資源獲取
由于Google官方未開放相關的服務,應用的資源下載,這時我們可以選擇再Open GApps網站上進行下載, Open GApps項目是一個開源項目,每晚(歐洲時間)更新一次Google相關的APP,包含適配各種架構,安卓版本的Google相關的服務,應用的資源文件。
版本的選擇
當我們進入Open GApps后可看到如下的選項界面,比較清晰,根據(jù)需求進行選擇下載:

針對不同的Variant包含的內容,網上已有介紹,這里也粘貼一下:
| 類型 | 描述 |
|---|---|
| aroma | 與super版所包含的GApps相同,但是在Recovery中引入了圖形化界面。 |
| super | 包含了所有GApps,像韓語日語中文拼音中文注音輸入法等。(請注意:如果你是用的是基于原生的ROM ,本版本會替換相機,通訊錄等等所有有關應用)。 |
| stock | 類似于Google Pixel出廠內置的GApps,相比super版少了其他語種的輸入法以及Google地球等。 |
| full | 與 stock 版所包含的內容相同,但此版本不會替換手機原本的應用. |
| mini | 包含基礎的 Google 服務框架,以及一些影響力較大的GApps,相比full版去掉了Docs等應用。 |
| micro | 包含基礎的Google服務框架和 Gmail 等常見GApps。 |
| nano | 包含基礎的Google服務框架,但不會有其他不必要的GApps。 |
| pico | 包含最迷你的Google服務框架,但由于框架并非完整,部分GApps可能無法運行 |
| tvstock | 此軟件包適用于Android TV設備。 它包括Nexus Player標配的所有GApps。 |
| tvmini | 適用于Android TV設備,只包含一些影響力較大的GApps。 |
這里我建議下一個pico版本和super,導入pico相關的內容后可以正常運行,根據(jù)需要去super版本內拿其余的apk。
資源包的解析
下載后解壓可看到如下目錄形式的文件結構:

其中
- Core是核心應用和資源的目錄,也就是我們需要預置的部分;
- GApps是其他的原生apk,根據(jù)需求判斷是否需要添加;
- Optional是一些可選的庫,可一并導入進系統(tǒng);
- tar-arm、unzip-arm、zip-arm是一些解壓縮的工具;
- 其余的文件還包含一些安裝腳本,可自動將相關的內容安裝至我們的手機中,由于我們是預置進系統(tǒng)內,所以不需要關注這部分的東西;
由于提取相關的apk和資源手動弄起來比較慢,這里提供一個我自己寫的腳本,放到解壓后的目錄的根目錄,然后執(zhí)行腳本就可以將所有我們需要的東西整理輸出到一個out目錄,我們再根據(jù)目錄的結構預置進我們的系統(tǒng)即可;
basepath=$(cd `dirname $0`; pwd)
mkdir -p out/priv-app
mkdir -p out/common
mkdir -p out/app
cd $basepath/Core && find . -name '*.lz' | xargs -n1 lzip -d && ls *.tar | xargs -n1 tar -xvf && cd ..
cd $basepath/GApps && find . -name '*.lz' | xargs -n1 lzip -d && ls *.tar | xargs -n1 tar -xvf && cd ..
cd $basepath/Optional && find . -name '*.lz' | xargs -n1 lzip -d && ls *.tar | xargs -n1 tar -xvf && cd ..
find -name priv-app | xargs -i cp -rf {} out/
find -name common | xargs -i cp -rf {} out/
find -name app | xargs -i cp -rf {} out/
cp g.prop out/common/etc/
執(zhí)行后對應的out目錄的內部結構如下:

這里介紹其中幾個比較重要的應用
- Google服務框架: GoogleServicesFramework;
- Google核心服務:PrebuiltGmsCore;
- GooglePlay:Phonesky;
注意:我們預置相關的應用,編寫相應Android.mk時,保留應用原有的簽名,不要使用我們的系統(tǒng)簽名去覆蓋它
GMS預置
GMS資源預置
priv-app的預置
預置Android.mk的編寫,由于應用較多,所以這里以一個應用為例,其他的依樣畫葫蘆即可:
##############GoogleServicesFramework##################
include $(CLEAR_VARS)
#
## Module name should match apk name to be installed
LOCAL_MODULE := GoogleServicesFramework
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := GoogleServicesFramework.apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
LOCAL_CERTIFICATE := PRESIGNED
LOCAL_DEX_PREOPT := true
LOCAL_PRIVILEGED_MODULE := true
include $(BUILD_PREBUILT)
app的預置
同樣的以一個應用為例
##############YouTube##################
include $(CLEAR_VARS)
## Module name should match apk name to be installed
LOCAL_MODULE := YouTube
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := YouTube.apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
LOCAL_CERTIFICATE := PRESIGNED
LOCAL_DEX_PREOPT := false
LOCAL_OVERRIDES_PACKAGES += \
YouTubeLeanback
include $(BUILD_PREBUILT)
common目錄的預置
common目錄內部的文件只需要拷貝進system目錄下即可,根據(jù)需要寫在對應的mk內
# google config
PRODUCT_COPY_FILES += $(call find-copy-subdir-files,*,xxxxxx/common,/system)
當所有都配置好之后編譯燒錄即可,當然前提是系統(tǒng)分區(qū)的空間充足,不然可能會編譯不通過;
之后燒錄進設備,聯(lián)網之后會出現(xiàn)設備未認證的提示,根據(jù)提示進行認證操作即可,這部分網上教程很多,這里也不展開。
AutoMotive系統(tǒng)GooglePlay預置問題
預置時需要注意除了GooglePlay以外基本都不太會受設備類型的影響,而GooglePlay根據(jù)不同的設備類型有不同的應用apk,內部可搜索的應用數(shù)量也跟設備內支持的feature有關(這部門的內容比較細,沒有研究的很深入,后續(xù)有機會再整理)。雖然有不同的apk,但它們對應的包名都為com.android.vending。
根據(jù)設備類型可分為如下幾種:
| 類型 | 描述 |
|---|---|
| 手機版Phonesky | 應用類型最多,最豐富的版本,我們所需要的也是這種版本。 |
| 電視版Tubesky | 電視版本的谷歌商店,經過測試,觸摸無法控制,需要靠外部的輸入設備(如鍵盤,鼠標,遙控器)等才可控制應用內部的內容,對觸摸設備沒做支持?所以也不考慮使用。 |
| 車機(AutoMotive)版 | 目前AutoMotive版本沒有對應的apk,我所用的apk為安裝手機版GooglePlay,然后由于系統(tǒng)存在AutoMotive的Feature,所以GooglePlay自動更新匹配我的設備,從而得來的apk(/data/app/com.android.vendingxxxx 目錄下),這種方式拿到的apk是被分包處理后的,若要預置到系統(tǒng)中,暫時還沒找到合適的方式;若只預置base.apk會出現(xiàn)某些資源缺失的問題,暫時不太可用。 |
| 手表版 | 為穿戴類安卓設備定制的谷歌商店,我這邊沒有研究,也就不展開。 |
那么如何在我們的車機預置手機版的GooglePlay:
預置不同于設備類型的GooglePlay
前面有提到GooglePlay受設備類型的影響,正常情況我們將手機版GooglePlay安裝在車機設備上是無法運行的,打開GooglePlay會有如下的彈窗:

此時其實后臺后自動幫我們將GooglePlay更新到車機版本,等待更新完成之后也能打開。

車機版GooglePlay界面
可以看到車機版的應用數(shù)量較少,可能不太符合我們的需求,所以需要兼容運行手機版GooglePlay。
通過jadx-gui對手機版GooglePlay即Phonesky.apk進行反編譯,搜索automotive我們可以看到內部有一些針對automotive, watch,tv等feature類型的判斷,雖然無法具體的看到是什么邏輯,但是我們可以大致猜測出GooglePlay的內容和啟動跟feature有一定的關系,這部分在官網也有一定的介紹。

這里我們取其中一個代碼來看一下:
package defpackage;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import com.google.firebase.FirebaseCommonRegistrar;
/* compiled from: PG */
/* renamed from: aepj reason: default package */
/* loaded from: classes.dex */
public final /* synthetic */ class aepj implements aetv {
private final /* synthetic */ int e;
public static final /* synthetic */ aepj d = new aepj(3);
public static final /* synthetic */ aepj c = new aepj(2);
public static final /* synthetic */ aepj b = new aepj(1);
public static final /* synthetic */ aepj a = new aepj(0);
private /* synthetic */ aepj(int i) {
this.e = i;
}
@Override // defpackage.aetv
public final String a(Object obj) {
int i = this.e;
if (i == 0) {
ApplicationInfo applicationInfo = ((Context) obj).getApplicationInfo();
return (applicationInfo == null || Build.VERSION.SDK_INT < 24) ? "" : String.valueOf(applicationInfo.minSdkVersion);
} else if (i == 1) {
ApplicationInfo applicationInfo2 = ((Context) obj).getApplicationInfo();
return applicationInfo2 != null ? String.valueOf(applicationInfo2.targetSdkVersion) : "";
} else if (i != 2) {
Context context = (Context) obj;
String installerPackageName = context.getPackageManager().getInstallerPackageName(context.getPackageName());
return installerPackageName != null ? FirebaseCommonRegistrar.a(installerPackageName) : "";
} else {
Context context2 = (Context) obj;
return context2.getPackageManager().hasSystemFeature("android.hardware.type.television") ?
"tv" : context2.getPackageManager().hasSystemFeature("android.hardware.type.watch") ?
"watch" : (Build.VERSION.SDK_INT < 23 || !context2.getPackageManager().hasSystemFeature("android.hardware.type.automotive")) ?
(Build.VERSION.SDK_INT < 26 || !context2.getPackageManager().hasSystemFeature("android.hardware.type.embedded")) ?
"" : "embedded" : "auto";
}
}
}
可以看到代碼內通過PackageManager的hasSystemFeature方法去判斷設備的類型,并返回對應的類型字符串,在源碼中找到對應的方法和文件(framework/base/core/java/android/app/ApplicationPackageManager.java),結合內部的方法來看,除了hasSystemFeature,還有一個獲得系統(tǒng)所有支持的Feature的方法:getSystemAvailableFeatures,我們同樣在反編譯后的代碼中搜索。

看到同樣有調用,那么我們就都一起改掉。
GMS適配修改方案
framework/base/core/java/android/app/ApplicationPackageManager.java
public class ApplicationPackageManager extends PackageManager {
....
public FeatureInfo[] getSystemAvailableFeatures() {
try {
ParceledListSlice<FeatureInfo> parceledList =
mPM.getSystemAvailableFeatures();
if (parceledList == null) {
return new FeatureInfo[0];
}
final List<FeatureInfo> list = parceledList.getList();
// add start ——————————————————————
if ("com.android.vending".equals(mContext.getPackageName()) || "com.google.android.gms".equals(mContext.getPackageName())) {
// googleplay and gms service rmove automotvie feature
for (int i = 0; i < list.size(); i++) {
if(PackageManager.FEATURE_AUTOMOTIVE.equals(list.get(i).name)) {
Log.i(TAG, "getSystemAvailableFeatures mContext.getPackageName() = " + mContext.getPackageName() + " i = " + i);
list.remove(i);
}
}
}
// add end ——————————————————————
final FeatureInfo[] res = new FeatureInfo[list.size()];
for (int i = 0; i < res.length; i++) {
res[i] = list.get(i);
}
return res;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@Override
public boolean hasSystemFeature(String name) {
return hasSystemFeature(name, 0);
}
@Override
public boolean hasSystemFeature(String name, int version) {
// add start ——————————————————————
if ("com.android.vending".equals(mContext.getPackageName()) || "com.google.android.gms".equals(mContext.getPackageName())) {
// googleplay and gms service rmove automotvie feature
Log.i(TAG, "hasSystemFeature mGmsFeatureType = " + mGmsFeatureType + " name = "+ name);
if (PackageManager.FEATURE_AUTOMOTIVE.equals(name)) {
return false;
}
}
// add end ——————————————————————
try {
return mPM.hasSystemFeature(name, version);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
邏輯很簡單,就是根據(jù)對應的包名移除對應的feature。之后再次編譯操作,就可以成功在我們的設備上運行手機版的GooglePlay了,運行效果如下:

GMS一次性認證方案
前面有提到,我們的設備燒錄之后都需要根據(jù)Google官方流程對設備進行驗證,如果是大量出貨的設備,若沒有與Google合作,那么每一臺都需要走一遍驗證流程,非常耗時和繁瑣,所以我們需要通過一些方式跳過這個認證的過程,保證我們的設備在燒錄之后不需要進行認證也可以使用谷歌服務,下面介紹一下具體的實現(xiàn)方式。
根據(jù)官網的認證操作流程,我們知道注冊認證的android_id的獲取方式如下:
adb root
abd shell
sqlite3 /data/data/com.google.android.gsf/databases/gservices.db
select * from main where name = "android_id";
所以我們只需要保證我們每臺設備的android_id都是我們已經認證過的設備id即可。
針對這一方案,可用的實現(xiàn)方案如下:
- 將已經認證設備的Google服務相關的初始數(shù)據(jù)庫 /data/data/com.google.android.gsf/databases 和 /data/data/com.google.android.gms/databases 復制出來;
- 將已認證的Google服務數(shù)據(jù)庫打包進系統(tǒng)固件中;
- 編寫腳本,第一次開機時啟動,使用已認證的Google服務數(shù)據(jù)庫覆蓋設備內對應的數(shù)據(jù)庫;
- 檢查設備android_id,確認成功替換,且沒有設備待認證的通知;