亚马逊语言识别Alexa之AlexaAndroid的接入及AlexaAndroid的源码解析(一)

一、用于国外关于语音识别的产品,现在亚马逊开发了Alexa给开发者使用。国内的话语言识别当然就科大讯飞莫属了,最近在接入亚马逊Alexa语音识别遇到很多在Stack Overflow中都没人解答的坑坑。在此以博客的形式记录自己深陷的坑和相应的解决办法。

整个接入的流程如图:


二、先把https://developer.amazon.com/zh/docs/login-with-amazon/install-sdk-android.html  中的亚马逊的登录开发文档的步骤浏览一遍整个过程。

    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是否正确的源码分析的流程。

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页