1. 程式人生 > >亞馬遜語言識別Alexa之AlexaAndroid的接入及AlexaAndroid的原始碼解析(一)

亞馬遜語言識別Alexa之AlexaAndroid的接入及AlexaAndroid的原始碼解析(一)

一、用於國外關於語音識別的產品,現在亞馬遜開發了Alexa給開發者使用。國內的話語言識別當然就科大訊飛莫屬了,最近在接入亞馬遜Alexa語音識別遇到很多在Stack Overflow中都沒人解答的坑坑。在此以部落格的形式記錄自己深陷的坑和相應的解決辦法。

整個接入的流程如圖:


    1.在這一步中要登入的話是要註冊產品,登入紅框位置的網址。按照描述的步驟進行:


    2.點選進入Alexa控制平臺https://developer.amazon.com/home.html 註冊自己的亞馬遜的賬戶登入.進入“應用與服務“->“用亞馬遜賬戶登入”->建立新的安全配置:

            

       2.1:下面的資訊可以以自己的需求填寫:

            

        2.2:如填寫“nihao”在manage下方選擇->Android/Kindle Settings填上自己新建專案的包名、應用程式的MD5和SHA256 ->Generate New Key這樣就可以拿到Android應用的api_key:


    2.3、在亞馬遜控臺上的ALEXA VOICE SERVICE中建立一個產品的ID。拿到下面步驟“三”中的PRODUCT_ID.


三、可以在Github上找到關於AlexaAndroid方面的相關專案:

    這裡有兩個專案地址:

            1.

官方AlexaAndroid地址:這是一個沒有api_key的專案是不執行起來的。

            2.持續更新的AlexaAndroid地址:這個專案是接入了api_key的,是一個能跑起來實現語音功能的專案。

四、關於“三”中專案2的原始碼分析:

   首先梳理一下整個Alexa語音輸入到語音應答在程式碼中體現的流程:

    ---->a.初始化AlexaManager:在這個初始化過程中會在AmaonAuthorizationManager建立時對assets下的api_key.txt的驗證,就是“二”中用APP應用的包名、MD5值、SHA256值在亞馬遜控制檯上生成的api_key。

    ---->b.初始化AlexaAudioPlayer語音播放的物件,用於播放從亞馬遜網路返回的AvsItem型別的音訊檔案。

   ---->c.如是關係到語音輸入的話:初始化RawAudioRecorder物件對使用者輸入的語音資訊進行收集加工處理,在AlexaManager方法sendAudioRequest()來進行語音資訊的傳送和接收來自亞馬遜的語音資訊。

    1.“a”中整個AlexaManager的初始化就是為在AmaonAuthorizationManager對api_key和PRODUCT_ID的驗證:

        下面是為驗證api_key及PRODUCT_ID所封裝管理類的程式碼:

private void initAlexaAndroid() {
    //get our AlexaManager instance for convenience
    //初始化AlexaManager驗證api_key及PRODUCT_ID
alexaManager = AlexaManager.getInstance(this, PRODUCT_ID);
//instantiate our audio player
audioPlayer = AlexaAudioPlayer.getInstance(this);
audioPlayer.addCallback(alexaAudioPlayerCallback);
}
private AlexaManager(Context context, String productId){
    mContext = context.getApplicationContext();
    if(productId == null){
        productId = context.getString(R.string.alexa_product_id);
}
    urlEndpoint = Util.getPreferences(context).getString(KEY_URL_ENDPOINT, context.getString(R.string.alexa_api));
//建立AuthorizationManager物件用來驗證api_key及productId
mAuthorizationManager = new AuthorizationManager(mContext, productId);
mAndroidSystemHandler = AndroidSystemHandler.getInstance(context);
Intent stickyIntent = new Intent(context, DownChannelService.class);
context.startService(stickyIntent);
    if(!Util.getPreferences(mContext).contains(IDENTIFIER)){
        Util.getPreferences(mContext)
                .edit()
                .putString(IDENTIFIER, createCodeVerifier(30))
                .apply();
}
}
public AuthorizationManager(@NotNull Context context, @NotNull String productId){
    mContext = context;
mProductId = productId;
    try {
        mAuthManager = new AmazonAuthorizationManager(mContext, Bundle.EMPTY);
}catch(IllegalArgumentException e){
        //This error will be thrown if the main project doesn't have the assets/api_key.txt file in it--this contains the security credentials from Amazon
Util.showAuthToast(mContext, "APIKey is incorrect or does not exist.");
Log.e(TAG, "Unable to Use Amazon Authorization Manager. APIKey is incorrect or does not exist. Does assets/api_key.txt exist in the main application?", e);
}
}

        最終的api_key的驗證是在AmaonAuthorizationManager進行的,在這裡有個大坑!!!---->>>>就是新建自己的應用程式時按照亞馬遜官方的在Android studio上獲取的MD5值和SHA256值加到“二、2.2”控制檯下生成的api_key的值在程式是編輯不過的會如上圖中彈出“APIKey is incorrect or does not exist”的toast。說明獲取到的MD5值和SHA256值是不對的。

---這個問題我起碼困擾了我一個星期。如果像我一樣根據MD5和SHA256在亞馬遜的控制檯上獲取到的api_key有誤的話,你可以按照我的笨方法去用斷點的方式去獲取到對應的MD5和SHA256值之後再安全配置中生成對的api_key:

    1.1、先拷貝獲取github專案中api_key.txt放入自己的專案中,在自己專案中的AmaonAuthorizationManager的構造方法:

public AmazonAuthorizationManager(Context context, Bundle options) {
    MAPLog.pii(LOG_TAG, "AmazonAuthorizationManager:sdkVer=3.0.0 libVer=3.5.3", "options=" + options);
    if(context == null) {
        throw new IllegalArgumentException("context must not be null!");
} else {
        this.mContext = context;
        if(options == null) {
            MAPLog.i(LOG_TAG, "Options bundle is null");
}
    //在這裡地方進行MD5和SHA256以及應用包名的驗證,進入appIdentifier.getAppInfo方法中
        AppInfo appInfo = appIdentifier.getAppInfo(this.mContext.getPackageName(), this.mContext);
        if(appInfo != null && appInfo.getClientId() != null) {
            this.clientId = appInfo.getClientId();
            if(options != null) {
                AuthorizationManager.setSandboxMode(context, options.getBoolean(BUNDLE_KEY.SANDBOX.val, false));
}

        } else {
            throw new IllegalArgumentException("Invalid API Key");
}
    }
}

    1.2、進入AbstractAppIdentifier中的getAppInfo()方法:

public AppInfo getAppInfo(String packageName, Context context) {
    MAPLog.i(LOG_TAG, "getAppInfo : packageName=" + packageName);
    return this.getAppInfoFromAPIKey(packageName, context);
}

public AppInfo getAppInfoFromAPIKey(String packageName, Context context) {
    MAPLog.i(LOG_TAG, "getAppInfoFromAPIKey : packageName=" + packageName);
    if(packageName == null) {
        MAPLog.w(LOG_TAG, "packageName can't be null!");
        return null;
} else {
        String apiKey = this.getAPIKey(packageName, context);//在getAPIKey會獲取到assets下api_key.txt
        return APIKeyDecoder.decode(packageName, apiKey, context);//進行應用中MD5及SHA256和api_key解析後的驗證
}
}

    1.3、這裡分步走先進入1.2中AbstractAppIdentifier的this.getAPIKey()方法中--->ThirdPartyResourceParser中的getApiKey()方法會返回_apiKey的值這個來源就是assets下的api_key.txt:

private String getAPIKey(String packageName, Context context) {
    MAPLog.i(LOG_TAG, "Finding API Key for " + packageName);
    assert packageName != null;
String to_return = null;
ThirdPartyResourceParser parser = null;
parser = this.getResourceParser(context, packageName);
to_return = parser.getApiKey();    //解析來自assets目錄下的api_key.txt
    return to_return;
}
public String getApiKey() {
    if(!this.isApiKeyInAssest()) {
        MAPLog.w(LOG_TAG, "Unable to get API Key from Assests");
String apiKey = this.getStringValueFromMetaData("APIKey");
        return apiKey != null?apiKey:this.getStringValueFromMetaData("AmazonAPIKey");
} else {
        return this._apiKey;    //會拿到全域性變數的api_key值
}
}
public ThirdPartyResourceParser(Context context, String packageName) {
    this._packageName = packageName;
    this._context = context;
    this._apiKey = this.parseApiKey();    //在ThirdPartyResourceParser構造方法中獲取_apiKey
}
private String parseApiKey() {
    if(this._context != null) {
        InputStream is = null;
        try {
            String var4;
            try {
                Resources resources = this._context.getPackageManager().getResourcesForApplication(this.getPackageName());
AssetManager assetManager = resources.getAssets();//很明顯獲取資產目錄下的檔案
is = assetManager.open(this.getApiKeyFile());    //獲取到檔名字api_key.txt
MAPLog.i(LOG_TAG, "Attempting to parse API Key from assets directory");
var4 = readString(is);
} finally {
                if(is != null) {
                    is.close();
}

            }

            return var4;
} catch (IOException var10) {
            MAPLog.i(LOG_TAG, "Unable to get api key asset document: " + var10.getMessage());
} catch (NameNotFoundException var11) {
            MAPLog.i(LOG_TAG, "Unable to get api key asset document: " + var11.getMessage());
}
    }

    return null;
}
protected String getApiKeyFile() {
    return "api_key.txt";    //在亞馬遜的文件中一定要以api_key.txt命名的原因
}

    1.4、跟上1.2及1.3的節奏1.2中的getAppInfo()的方法走1.3的路線拿到放置在assets下的String流,之後傳入APIKeyDecoder.decode中進行兩者的效驗:

public static AppInfo decode(String packageName, String apiKey, Context context) {
    return doDecode(packageName, apiKey, true, context);
}

static AppInfo doDecode(String packageName, String apiKey, boolean verifyPayload, Context context) {
    MAPLog.i(LOG_TAG, "Begin decoding API Key for packageName=" + packageName);
JSONObject payload = (new JWTDecoder()).decode(apiKey); //把assets下的檔案解析成payload
MAPLog.pii(LOG_TAG, "APIKey", "payload=" + payload);
    if(payload == null) {
        MAPLog.w(LOG_TAG, "Unable to decode APIKey for pkg=" + packageName);
        return null;
} else {
        try {
            if(verifyPayload) {
                verifyPayload(packageName, payload, context);   //在這裡設定斷點,進行斷點除錯獲取對應的
}

            return extractAppInfo(payload);
} catch (SecurityException var6) {
            MAPLog.w(LOG_TAG, "Failed to decode: " + var6.getMessage());
} catch (NameNotFoundException var7) {
            MAPLog.w(LOG_TAG, "Failed to decode: " + var7.getMessage());
} catch (CertificateException var8) {
            MAPLog.w(LOG_TAG, "Failed to decode: " + var8.getMessage());
} catch (NoSuchAlgorithmException var9) {
            MAPLog.w(LOG_TAG, "Failed to decode: " + var9.getMessage());
} catch (JSONException var10) {
            MAPLog.w(LOG_TAG, "Failed to decode: " + var10.getMessage());
} catch (IOException var11) {
            MAPLog.w(LOG_TAG, "Failed to decode: " + var11.getMessage());
} catch (AuthError var12) {
            MAPLog.w(LOG_TAG, "Failed to decode: " + var12.getMessage());
}

        MAPLog.w(LOG_TAG, "Unable to decode APIKey for pkg=" + packageName);
        return null;
}
}

    斷點除錯到這一步就需要把verifySignature裡中的signaturesFromAndroid陣列元素記錄下來,如MD5值是什麼?SHA256是什麼?存有這兩個值填入到亞馬遜控制檯下的安全配置上的對應選項中再生成api_key.。這樣就可以準確無誤的拿到可以驗證效驗的api_key值。

private static void verifyPayload(String packageName, JSONObject payload, Context context) throws SecurityException, JSONException, NameNotFoundException, CertificateException, NoSuchAlgorithmException, IOException {
    MAPLog.i(LOG_TAG, "verifyPayload for packageName=" + packageName);
    if(!payload.getString("iss").equals("Amazon")) {
        throw new SecurityException("Decoding fails: issuer (" + payload.getString("iss") + ") is not = " + "Amazon" + " pkg=" + packageName);
} else if(packageName != null && !packageName.equals(payload.getString("pkg"))) {
        throw new SecurityException("Decoding fails: package names don't match! - " + packageName + " != " + payload.getString("pkg"));
} else {
        String signatureSha256FromAPIKey;    //斷點除錯顯示這個值是assets下的MD5或SHA256值
        if(payload.has("appsig")) {
            signatureSha256FromAPIKey = payload.getString("appsig");
MAPLog.pii(LOG_TAG, "Validating MD5 signature in API key", String.format("pkg = %s and signature %s", new Object[]{packageName, signatureSha256FromAPIKey}));
verifySignature(signatureSha256FromAPIKey, packageName, HashAlgorithm.MD5, context);
}

        if(payload.has("appsigSha256")) {
            signatureSha256FromAPIKey = payload.getString("appsigSha256");
MAPLog.pii(LOG_TAG, "Validating SHA256 signature in API key", String.format("pkg = %s and signature %s", new Object[]{packageName, signatureSha256FromAPIKey}));
verifySignature(signatureSha256FromAPIKey, packageName, HashAlgorithm.SHA_256, context);
}

    }
}

private static void verifySignature(String signatureFromAPIKey, String packageName, HashAlgorithm hashAlgorithm, Context context) {
    if(signatureFromAPIKey == null) {
        MAPLog.d(LOG_TAG, "App Signature is null. pkg=" + packageName);
        throw new SecurityException("Decoding failed: certificate fingerprint can't be verified! pkg=" + packageName);
} else {    //下面的signaturesFromAndroid陣列元素就是本應用的MD5和SHA256正確值,可把這兩者取出填寫到亞馬遜控制檯下
        String signature = signatureFromAPIKey.replace(":", "");
List<String> signaturesFromAndroid = PackageSignatureUtil.getAllSignaturesFor(packageName, hashAlgorithm, context); 
    MAPLog.i(LOG_TAG, "Number of signatures = " + signaturesFromAndroid.size());
MAPLog.pii(LOG_TAG, "Fingerprint checking", signaturesFromAndroid.toString());
        if(!signaturesFromAndroid.contains(signature.toLowerCase(Locale.US))) {    //兩者對不上丟擲異常
            throw new SecurityException("Decoding failed: certificate fingerprint can't be verified! pkg=" + packageName);
}
    }
}

    斷點效果如圖為:



上面兩個圖中打到斷點到這一步,則分別會獲取到signaturesFromAndroid關於MD5及SHA256的正確無誤的值。不過在這個值之間加上“:”,如01:4A:3E:6B:D7:07:64:2B:36:0A:2A:0C:D2:0C:04:0C。獲取到SHA256的方法也是類似的,在獲取到正確的MD5值之後程式碼不丟擲異常接下來就可以 接著用上述流程獲取SHA256的正確值了。

以上就是本人獲取到能在自己新建專案中校驗正確的api_key.txt的做法,及校驗api_key.txt是否正確的原始碼分析的流程。