文章目录
- 1、申请账户、填写配置文件等
- 2、github 上的项目
- 3、主要功能代码介绍
- 1)alexa 语音识别流程
- 2)验证api_key PRODUCT_ID 等相关信息
- 3) 登录亚马逊的模块
- 4、用户语音的收集过程:
- 5、收集到语音之后以数据流拼接成http的请求通过网络发送到Alexa:
- 5、网络响应返回的数据的解析
1、申请账户、填写配置文件等
1)对于一些账户的申请注册
2)对于apk 登陆访问权限的申请注册
3)对于product id api_key.txt等的申请注册
请参考:
如何让自己的Apk 能够登录亚马逊账户
alexa/alexa-avs-sample-app Windows 安装教程
2、github 上的项目
这里有两个项目都是github 上的:
官方的 AlexaAndroid :
willblaschko/AlexaAndroid 只是这个不能直接运行,因为没有api_key.txt 文件,也没有product id 等 相关信息,都需要自己重更新填写。
这一个包含编译好的apk:
evil0327/Alexa-On-Android
不方便github 登录或者在次下载:Alexa android
3、主要功能代码介绍
1)alexa 语音识别流程
(1) 初始化AlexaManager:
在这个初始化的过程中AmaonAuthorizationManager创建时对assets文件下的api_key.txt文件进行验证,验证app 的相关数值:包名、 MD5 、 SHA256等, 验证是否是在亚马逊官网注册过了。
只有注册过了,才能够能录亚马逊官网成功,登陆成功后,亚马逊会返回一个访问亚马逊api 的一个访问令牌,接着apk 才可以通过访问令牌 与 avs 进行数据的交互。
(2)初始化RawAudioRecorder对象
RawAudioRecorder 对象对用户输入的语音信息进行收集加工处理,在AlexaManager 方法sendAudioRequest()来进行语音信息的发送和接受来自亚马逊的语音信息。
(3)初始化AlexaAudioPlayer语音播放对象,用于播放从亚马逊返回的AvsItem类型的音频文件。
2)验证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值加到“”控制台下生成的api_key的值在程序是编辑不过的会如上图中弹出“APIKey is incorrect or does not exist”的toast。说明获取到的MD5值和SHA256值是不对的。
—如果像我一样根据MD5和SHA256在亚马逊的控制台上获取到的api_key有误的话,你可以按照用断点的方式去获取到对应的MD5和SHA256值之后再安全配置中生成对的api_key:
以下文件都在 加载的 第三方 SDK 中,就是 那个 login-amazon-sdk.rar
(1)、先拷贝获取github项目中api_key.txt放入自己的项目中,在自己项目中的AmaonAuthorizationManager.class的构造方法:
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");
}
}
}
(2)、进入AbstractAppIdentifier.class中的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解析后的验证
}
}
(3)这里分步走先进入1.2中AbstractAppIdentifier.class的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命名的原因
}
(4)跟上2及3的节奏2中的getAppInfo()的方法走3的路线拿到放置在assets下的String流,之后传入APIKeyDecoder.class中进行两者的效验:
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是否正确的源码分析的流程。
3) 登录亚马逊的模块
首先,在通过网络发送语音数据流到Alexa后台时,需要对账号进行登录验证。以下是以AlexaAndroid开源项目的源码分析。
(1)登录验证则要跳转到浏览器进行登录验证。在AndroidManifest.xml对AuthorizationActivity注册。
<activity
android:name="com.amazon.identity.auth.device.authorization.AuthorizationActivity"
android:allowTaskReparenting="true"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- host should be our application package //-->
<data
android:host="com.willblaschko.android.avs"
android:scheme="amzn" />
</intent-filter>
</activity>
(2)在SendAudioActionFragment中的alexaManager.sendAudioRequest()方法中验证了账号的登录,在发送授权token之前是要对账号登录验证:
alexaManager.sendAudioRequest(requestBody, getRequestCallback());
public void sendAudioRequest(final DataRequestBody requestBody, @Nullable final AsyncCallback<AvsResponse, Exception> callback){
//check if the user is already logged in
mAuthorizationManager.checkLoggedIn(mContext, new ImplCheckLoggedInCallback() {
@Override
public void success(Boolean result) {
if (result) {
//if the user is logged in
//set our URL
final String url = getEventsUrl();
//get our access token
TokenManager.getAccessToken(mAuthorizationManager.getAmazonAuthorizationManager(), mContext, new TokenManager.TokenCallback() {
@Override
public void onSuccess(final String token) {
//do this off the main thread
new AsyncTask<Void, Void, AvsResponse>() {
@Override
protected AvsResponse doInBackground(Void... params) {
try {
getSpeechSendAudio().sendAudio(url, token, requestBody, new AsyncEventHandler(AlexaManager.this, callback));
} catch (IOException e) {
e.printStackTrace();
//bubble up the error
if(callback != null) {
callback.failure(e);
}
}
return null;
}
@Override
protected void onPostExecute(AvsResponse avsResponse) {
super.onPostExecute(avsResponse);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public void onFailure(Throwable e) {
}
});
} else {
//if the user is not logged in, log them in and then call the function again
logIn(new ImplAuthorizationCallback<AvsResponse>(callback) {
@Override
public void onSuccess() {
//call our function again
sendAudioRequest(requestBody, callback);
}
});
}
}
});
}
public void logIn(@Nullable final AuthorizationCallback callback){
//check if we're already logged in
mAuthorizationManager.checkLoggedIn(mContext, new AsyncCallback<Boolean, Throwable>() {
@Override
public void start() {
}
@Override
public void success(Boolean result) {
//if we are, return a success
if(result){
if(callback != null){
callback.onSuccess();
}
}else{
//otherwise start the authorization process
//拼接授权登录的URL到AuthorizationActivity下登录验证
mAuthorizationManager.authorizeUser(callback);
}
}
@Override
public void failure(Throwable error) {
if(callback != null) {
callback.onError(new Exception(error));
}
}
@Override
public void complete() {
}
});
}
(3) 2.AuthorizationManager的AuthorizeUser()方法中会拼接URL所需要的数据:
public void authorizeUser(AuthorizationCallback callback){
mCallback = callback;
String PRODUCT_DSN = Settings.Secure.getString(mContext.getContentResolver(), Settings.Secure.ANDROID_ID);
Bundle options = new Bundle();
String scope_data = "{\"alexa:all\":{\"productID\":\"" + mProductId +
"\", \"productInstanceAttributes\":{\"deviceSerialNumber\":\"" +PRODUCT_DSN + "\"}}}";
options.putString(AuthzConstants.BUNDLE_KEY.SCOPE_DATA.val, scope_data);
options.putBoolean(AuthzConstants.BUNDLE_KEY.GET_AUTH_CODE.val, true);
options.putString(AuthzConstants.BUNDLE_KEY.CODE_CHALLENGE.val, getCodeChallenge());
options.putString(AuthzConstants.BUNDLE_KEY.CODE_CHALLENGE_METHOD.val, "S256");
//拼接URL数据规则后传递给在login-with-amazon-sdk.jar中的AmazonAuthorizationManager处理
mAuthManager.authorize(APP_SCOPES, options, authListener);
}
(4)数据由亚马逊账号登录的jar包ogin-with-amazon-sdk.jar中类AmazonAuthorizationManager -->InternalAuthManager中的authorize()方法中:
/** @deprecated */
@Deprecated
public Future<Bundle> authorize(String[] scopes, Bundle options, AuthorizationListener listener) {
return InternalAuthManager.getInstance(this.mContext).authorize((AuthorizeRequest)null, this.mContext, scopes, options, listener);
}
public Future<Bundle> authorize(final AuthorizeRequest request, final Context context, final String[] scopes, final Bundle options, final AuthorizationListener listener) {
if(scopes != null && scopes.length != 0) {
MAPLog.i(LOG_TAG, context.getPackageName() + " calling authorize: scopes=" + Arrays.toString(scopes));
ThreadUtils.THREAD_POOL.execute(new Runnable() {
public void run() {
if(!InternalAuthManager.this.isAPIKeyValid(context)) {
listener.onError(new AuthError("APIKey is invalid", ERROR_TYPE.ERROR_ACCESS_DENIED));
} else {
Bundle allOptions = options == null?new Bundle():new Bundle(options);
if(!allOptions.containsKey(BUNDLE_KEY.SANDBOX.val)) {
allOptions.putBoolean(BUNDLE_KEY.SANDBOX.val, AuthorizationManager.isSandboxMode(context));
}
//创建第三方授权登录的帮助类
ThirdPartyAuthorizationHelper authzHelper = new ThirdPartyAuthorizationHelper();
try {//从这里可启动浏览器进行账号的登录验证:
authzHelper.authorize(request, context, context.getPackageName(), InternalAuthManager.this.clientId, InternalAuthManager.this.getRedirectURI(context), scopes, true, InternalAuthManager.tokenVendor, listener, allOptions);
} catch (AuthError var4) {
listener.onError(var4);
}
}
}
});
return null;
} else {
throw new IllegalArgumentException("scopes must not be null or empty!");
}
}
(5)ThirdPartyAuthorizationHelper的authorize()方法中:
public void authorize(final AuthorizeRequest originalRequest, final Context context, String packageName, final String clientId, String redirectURI, String[] requestedScopes, final boolean isBrowserFlow, TokenVendor tokenVendor, final AuthorizationListener listener, Bundle options) throws AuthError {
if(ThreadUtils.isRunningOnMainThread()) {
MAPLog.e(LOG_TAG, "authorize started on main thread");
throw new IllegalStateException("authorize started on main thread");
} else {
AppIdentifier appIdentifier = new ThirdPartyAppIdentifier();
final AppInfo appInfo = appIdentifier.getAppInfo(packageName, context);
List<RequestedScope> cachedScopes = tokenVendor.getCachedScopes(context);
final String[] allScopes = getCommonScopesForAuthorization(context, requestedScopes, cachedScopes);
final boolean isSandboxMode = options.getBoolean(BUNDLE_KEY.SANDBOX.val, false);
final Bundle extraParameters;
if(options == Bundle.EMPTY) {
extraParameters = new Bundle();
} else {
extraParameters = options;
}
extraParameters.putBoolean(BUNDLE_KEY.CHECK_API_KEY.val, false);
extraParameters.putBoolean(BUNDLE_KEY.RETURN_CODE.val, true);
extraParameters.putString(AUTHORIZE_BUNDLE_KEY.REGION.val, AuthorizationManager.getRegion(context).getStringValue());
extraParameters.putString(BUNDLE_KEY.CLIENT_ID.val, clientId);
extraParameters.putString(BUNDLE_KEY.SDK_VERSION.val, "LWAAndroidSDK3.0.0");
try {
extraParameters.putBundle(BUNDLE_KEY.EXTRA_URL_PARAMS.val, this.getExtraUrlParams(extraParameters));
} catch (AuthError var19) {
listener.onError(var19);
return;
}
Bundle results = Bundle.EMPTY;
if(!isSandboxMode && (StoredPreferences.isTokenObtainedFromSSO(context) || cachedScopes == null || cachedScopes.size() == 0)) {
results = this.startAuthorizationWithService(context, allScopes, extraParameters);
}
if(results.containsKey("code") && !TextUtils.isEmpty(results.getString("code"))) {
if(extraParameters.getBoolean(BUNDLE_KEY.GET_AUTH_CODE.val, false)) {
AuthorizationHelper.sendAuthorizationCodeAsResponse(results.getString("code"), clientId, redirectURI, listener);
return;
}
String codeVerifier = this.codeChallengeWorkflow.getCodeVerifier();
this.handleCodeForTokenExchange(context, packageName, codeVerifier, results, extraParameters, listener);
StoredPreferences.setTokenObtainedFromSSO(context, true);
} else if(!results.containsKey("AUTH_ERROR_EXECEPTION") && !results.containsKey(BUNDLE_KEY.AUTHORIZE.val) && !results.containsKey(BUNDLE_KEY.CAUSE_ID.val)) {
ProfileDataSource.getInstance(context).deleteAllRows();
Handler myHandler = new Handler(Looper.getMainLooper());
myHandler.post(new Runnable() {
public void run() {
try {
if(!isBrowserFlow && !isSandboxMode) {
listener.onError(new AuthError("WebView is not allowed for Authorization", ERROR_TYPE.ERROR_BAD_PARAM));
} else {//跟Browser交互登录验证的方法:
ThirdPartyAuthorizationHelper.this.authorizeWithBrowser(originalRequest, context, context.getPackageName(), clientId, allScopes, listener, extraParameters, appInfo);
StoredPreferences.setTokenObtainedFromSSO(context, false);
}
} catch (AuthError var2) {
listener.onError(var2);
}
}
});
} else {
results.setClassLoader(context.getClassLoader());
if(results.containsKey(BUNDLE_KEY.CAUSE_ID.val)) {
listener.onCancel(results);
} else if(results.containsKey("AUTH_ERROR_EXECEPTION")) {
listener.onError(AuthError.extractError(results));
} else {
DatabaseHelper.clearAuthorizationState(context);
Bundle bundle = new Bundle();
bundle.putString(BUNDLE_KEY.AUTHORIZE.val, "authorized via service");
listener.onSuccess(bundle);
}
}
}
}
private void authorizeWithBrowser(AuthorizeRequest originalRequest, Context context, String packageName, String clientId, String[] scopes, AuthorizationListener listener, Bundle options, AppInfo appInfo) throws AuthError {
options.getBundle(BUNDLE_KEY.EXTRA_URL_PARAMS.val).remove("client_id");
AuthorizationRequest request = new AuthorizationRequest(originalRequest, clientId, scopes, options, appInfo, listener);
RequestManager.getInstance().executeRequest(request, context); //执行浏览器的请求
}
(6)RequestManager中的executeRequest():
public void executeRequest(AbstractRequest request, Context context) throws AuthError {
MAPLog.d(LOG_TAG, "Executing request " + request.getRequestId());
if(!request.canAttempt()) {
throw new AuthError(String.format("Reached maximum attempts for the request: %s", new Object[]{request.getRequestId()}), ERROR_TYPE.ERROR_SERVER_REPSONSE);
} else {
request.incrementAttemptCount();
this.cleanupOldActiveRequests();
this.activeRequests.put(request.getRequestId(), request);
this.externalBrowserManager.openUrl(request, request.getUrl(context), context);//这里打开浏览器传入Url
}
}
(7)最后的操作是在ExternalBrowserManager中的openUrl()用intent启动浏览器进行登录验证:
public void openUrl(AbstractRequest request, String url, Context context) throws AuthError {
CompatibilityUtil.assertCorrectManifestIntegration(context); //manifest注册的activity
Intent intent = this.getIntent(url, context);
MAPLog.i(LOG_TAG, "Starting External Browser");
try {
request.onStart();
context.startActivity(intent); //打开activity
} catch (Exception var6) {
MAPLog.e(LOG_TAG, "Unable to Launch Browser: " + var6.getMessage());
throw new AuthError("Unable to Launch Browser.", var6, ERROR_TYPE.ERROR_UNKNOWN);
}
}
(8)如在浏览器登录成功之后返回在AuthorizationManager的AuthorizeUser()的回调中TokenManager保存token的值及刷新token值:
private AuthorizationListener authListener = new AuthorizationListener() {
/**
* Authorization was completed successfully.
* Display the profile of the user who just completed authorization
* @param response bundle containing authorization response. Not used.
*/
@Override
public void onSuccess(Bundle response) {
String authCode = response.getString(AuthzConstants.BUNDLE_KEY.AUTHORIZATION_CODE.val);
if(BuildConfig.DEBUG) {
Log.i(TAG, "Authorization successful");
Util.showAuthToast(mContext, "Authorization successful.");
}
//登录回调成功的处理进行token解析及保存
TokenManager.getAccessToken(mContext, authCode, getCodeVerifier(), mAuthManager, new TokenManager.TokenResponseCallback() {
@Override
public void onSuccess(TokenManager.TokenResponse response) {
if(mCallback != null){
mCallback.onSuccess();
}
}
@Override
public void onFailure(Exception error) {
if(mCallback != null){
mCallback.onError(error);
}
}
});
}
public static void getAccessToken(final Context context, @NotNull String authCode, @NotNull String codeVerifier, AmazonAuthorizationManager authorizationManager, @Nullable final TokenResponseCallback callback){
//this url shouldn't be hardcoded, but it is, it's the Amazon auth access token endpoint
String url = "https://api.amazon.com/auth/O2/token";
//set up our arguments for the api call, these will be the call headers
FormBody.Builder builder = new FormBody.Builder()
.add(ARG_GRANT_TYPE, "authorization_code")
.add(ARG_CODE, authCode);
try {
builder.add(ARG_REDIRECT_URI, authorizationManager.getRedirectUri());
builder.add(ARG_CLIENT_ID, authorizationManager.getClientId());
} catch (AuthError authError) {
authError.printStackTrace();
}
builder.add(ARG_CODE_VERIFIER, codeVerifier);
OkHttpClient client = ClientUtil.getTLS12OkHttpClient();
Request request = new Request.Builder()
.url(url)
.post(builder.build())
.build();
final Handler handler = new Handler(Looper.getMainLooper());
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
e.printStackTrace();
if(callback != null){
//bubble up error
handler.post(new Runnable() {
@Override
public void run() {
callback.onFailure(e);
}
});
}
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String s = response.body().string();
if(BuildConfig.DEBUG) {
Log.i(TAG, s);
}
final TokenResponse tokenResponse = new Gson().fromJson(s, TokenResponse.class);
//save our tokens to local shared preferences 保存回调回来的token的值
saveTokens(context, tokenResponse);
if(callback != null){
//bubble up success
handler.post(new Runnable() {
@Override
public void run() {
callback.onSuccess(tokenResponse);
}
});
}
}
});
}
4、用户语音的收集过程:
(1).首先是初始话RawAudioRecorder对象,用回调DataRequestBody收集到用户的语音输入:
private DataRequestBody requestBody = new DataRequestBody() {
@Override
public void writeTo(BufferedSink sink) throws IOException {
while (recorder != null && !recorder.isPausing()) { //以此判断用户输入是否完成
if(recorder != null) {
final float rmsdb = recorder.getRmsdb();
if(recorderView != null) {
recorderView.post(new Runnable() {
@Override
public void run() {
recorderView.setRmsdbLevel(rmsdb);
}
});
}
if(sink != null && recorder != null) {
sink.write(recorder.consumeRecording());
}
if(BuildConfig.DEBUG){
Log.i(TAG, "Received audio");
Log.e(TAG, "RMSDB: " + rmsdb);
}
}
try {
Thread.sleep(25);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
stopListening();
}
};
5、收集到语音之后以数据流拼接成http的请求通过网络发送到Alexa:
(1).AlexaManager获取token成功之后:
//get our access token
TokenManager.getAccessToken(mAuthorizationManager.getAmazonAuthorizationManager(), mContext, new TokenManager.TokenCallback() {
@Override
public void onSuccess(final String token) {
//do this off the main thread
new AsyncTask<Void, Void, AvsResponse>() {
@Override
protected AvsResponse doInBackground(Void... params) {
try { //拼接requestBody发送语音流数据
getSpeechSendAudio().sendAudio(url, token, requestBody, new AsyncEventHandler(AlexaManager.this, callback));
} catch (IOException e) {
e.printStackTrace();
//bubble up the error
if(callback != null) {
callback.failure(e);
}
}
return null;
}
@Override
protected void onPostExecute(AvsResponse avsResponse) {
super.onPostExecute(avsResponse);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public void onFailure(Throwable e) {
}
});
(2)SpeechSendAudio类中sendAudio()方法:
public void sendAudio(final String url, final String accessToken, @NotNull DataRequestBody requestBody,
final AsyncCallback<Call, Exception> callback) throws IOException {
this.requestBody = requestBody; //请求体会在SpeechSendEvent以audio文件格式添加
if(callback != null){
callback.start();
}
Log.i(TAG, "Starting SpeechSendAudio procedure");
start = System.currentTimeMillis();
//call the parent class's prepareConnection() in order to prepare our URL POST
try {
prepareConnection(url, accessToken); //拼接好请求头所需头数据
final Call response = completePost(); //返回响应的数据
if (callback != null) {
if (response != null) {
callback.success(response);
}
callback.complete();
}
Log.i(TAG, "Audio sent");
Log.i(TAG, "Audio sending process took: " + (System.currentTimeMillis() - start));
} catch (IOException|AvsException e) {
onError(callback, e);
}
}
(3)SpeechSendAudio父类sendEvent中请求和响应做出了处理:
protected void prepareConnection(String url, String accessToken) {
//set the request URL
mRequestBuilder.url(url);
//set our authentication access token header
mRequestBuilder.addHeader("Authorization", "Bearer " + accessToken);
String event = getEvent();
mBodyBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("metadata", "metadata", RequestBody.create(MediaType.parse("application/json; charset=UTF-8"), event));
//reset our output stream
mOutputStream = new ByteArrayOutputStream();
}
protected Call completePost() throws IOException, AvsException, RuntimeException {
addFormDataParts(mBodyBuilder);
mRequestBuilder.post(mBodyBuilder.build());
return parseResponse();
}
5、网络响应返回的数据的解析
(1)getRequstCallback(),BaseActivity用异步请求回调的方式:
alexaManager.sendAudioRequest(requestBody, getRequestCallback());
//async callback for commands sent to Alexa Voice
private AsyncCallback<AvsResponse, Exception> requestCallback = new AsyncCallback<AvsResponse, Exception>() {
@Override
public void start() {
startTime = System.currentTimeMillis();
Log.i(TAG, "Event Start");
setState(STATE_PROCESSING);
}
@Override
public void success(AvsResponse result) {
Log.i(TAG, "Event Success");
//解析从Alexa返回的数据
Log.e(TAG, "success:处理从Alexa返回回来的数据:"+result.toString());
handleResponse(result);
}
@Override
public void failure(Exception error) {
error.printStackTrace();
Log.i(TAG, "Event Error");
setState(STATE_FINISHED);
}
@Override
public void complete() {
Log.i(TAG, "Event Complete");
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
long totalTime = System.currentTimeMillis() - startTime;
Toast.makeText(BaseActivity.this, "Total request time: " + totalTime + " miliseconds", Toast.LENGTH_LONG).show();
//Log.i(TAG, "Total request time: "+totalTime+" miliseconds");
}
});
}
};
(2) handleResonse()对返回数据检查,保存在List数组中:
private void handleResponse(AvsResponse response) {
boolean checkAfter = (avsQueue.size() == 0);
if (response != null) {
//if we have a clear queue item in the list, we need to clear the current queue before proceeding
//iterate backwards to avoid changing our array positions and getting all the nasty errors that come
//from doing that
for (int i = response.size() - 1; i >= 0; i--) {
if (response.get(i) instanceof AvsReplaceAllItem || response.get(i) instanceof AvsReplaceEnqueuedItem) {
//clear our queue
avsQueue.clear();
//remove item
response.remove(i);
}
}
Log.i(TAG, "Adding " + response.size() + " items to our queue");
if (BuildConfig.DEBUG) {
for (int i = 0; i < response.size(); i++) {
Log.i(TAG, "\tAdding: " + response.get(i).getToken());
}
}
avsQueue.addAll(response);
}
if (checkAfter) {
checkQueue();
}
}
(3) 而在chekQueue中对AvsItem子类类型分别做了处理:
private void checkQueue() {
//if we're out of things, hang up the phone and move on
if (avsQueue.size() == 0) {
setState(STATE_FINISHED);
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
long totalTime = System.currentTimeMillis() - startTime;
Toast.makeText(BaseActivity.this, "Total interaction time: " + totalTime + " miliseconds", Toast.LENGTH_LONG).show();
Log.i(TAG, "Total interaction time: " + totalTime + " miliseconds");
}
});
return;
}
final AvsItem current = avsQueue.get(0);
Log.i(TAG, "Item type " + current.getClass().getName());
if (current instanceof AvsPlayRemoteItem) {
//play a URL
if (!audioPlayer.isPlaying()) {
audioPlayer.playItem((AvsPlayRemoteItem) current);
}
} else if (current instanceof AvsPlayContentItem) {
//play a URL
if (!audioPlayer.isPlaying()) {
audioPlayer.playItem((AvsPlayContentItem) current);
}
} else if (current instanceof AvsSpeakItem) {
//play a sound file
if (!audioPlayer.isPlaying()) {
audioPlayer.playItem((AvsSpeakItem) current);
}
setState(STATE_SPEAKING);
} else if (current instanceof AvsStopItem) {
//stop our play
audioPlayer.stop();
avsQueue.remove(current);
} else if (current instanceof AvsReplaceAllItem) {
//clear all items
//mAvsItemQueue.clear();
audioPlayer.stop();
avsQueue.remove(current);
} else if (current instanceof AvsReplaceEnqueuedItem) {
//clear all items
//mAvsItemQueue.clear();
avsQueue.remove(current);
} else if (current instanceof AvsExpectSpeechItem) {
//listen for user input
audioPlayer.stop();
avsQueue.clear();
startListening();
} else if (current instanceof AvsSetVolumeItem) {
//set our volume
setVolume(((AvsSetVolumeItem) current).getVolume());
avsQueue.remove(current);
} else if (current instanceof AvsAdjustVolumeItem) {
//adjust the volume
adjustVolume(((AvsAdjustVolumeItem) current).getAdjustment());
avsQueue.remove(current);
} else if (current instanceof AvsSetMuteItem) {
//mute/unmute the device
setMute(((AvsSetMuteItem) current).isMute());
avsQueue.remove(current);
} else if (current instanceof AvsMediaPlayCommandItem) {
//fake a hardware "play" press
sendMediaButton(this, KeyEvent.KEYCODE_MEDIA_PLAY);
Log.i(TAG, "Media play command issued");
avsQueue.remove(current);
} else if (current instanceof AvsMediaPauseCommandItem) {
//fake a hardware "pause" press
sendMediaButton(this, KeyEvent.KEYCODE_MEDIA_PAUSE);
Log.i(TAG, "Media pause command issued");
avsQueue.remove(current);
} else if (current instanceof AvsMediaNextCommandItem) {
//fake a hardware "next" press
sendMediaButton(this, KeyEvent.KEYCODE_MEDIA_NEXT);
Log.i(TAG, "Media next command issued");
avsQueue.remove(current);
} else if (current instanceof AvsMediaPreviousCommandItem) {
//fake a hardware "previous" press
sendMediaButton(this, KeyEvent.KEYCODE_MEDIA_PREVIOUS);
Log.i(TAG, "Media previous command issued");
avsQueue.remove(current);
} else if (current instanceof AvsResponseException) {
runOnUiThread(new Runnable() {
@Override
public void run() {
new AlertDialog.Builder(BaseActivity.this)
.setTitle("Error")
.setMessage(((AvsResponseException) current).getDirective().getPayload().getCode() + ": " + ((AvsResponseException) current).getDirective().getPayload().getDescription())
.setPositiveButton(android.R.string.ok, null)
.show();
}
});
avsQueue.remove(current);
checkQueue();
}
}
(4) AlexaAudioPlayer中playItem()方法根据不同的AvsItem子类类型用MediaPlayer播放语音:
private void play(AvsItem item){
if(isPlaying()){
Log.w(TAG, "Already playing an item, did you mean to play another?");
}
mItem = item;
if(getMediaPlayer().isPlaying()){
//if we're playing, stop playing before we continue
getMediaPlayer().stop();
}
//reset our player
getMediaPlayer().reset();
if(!TextUtils.isEmpty(mItem.getToken()) && mItem.getToken().contains("PausePrompt")){
//a gross work around for a broke pause mp3 coming from Amazon, play the local mp3
try {
AssetFileDescriptor afd = mContext.getAssets().openFd("shhh.mp3");
getMediaPlayer().setDataSource(afd.getFileDescriptor(),afd.getStartOffset(),afd.getLength());
} catch (IOException e) {
e.printStackTrace();
//bubble up our error
bubbleUpError(e);
}
}else if(mItem instanceof AvsPlayRemoteItem){
//cast our item for easy access
AvsPlayRemoteItem playItem = (AvsPlayRemoteItem) item;
try {
//set stream
getMediaPlayer().setAudioStreamType(AudioManager.STREAM_MUSIC);
//play new url
Log.e(TAG, "播放音频流1为:"+playItem.getUrl());
getMediaPlayer().setDataSource(playItem.getUrl());
} catch (IOException e) {
e.printStackTrace();
//bubble up our error
bubbleUpError(e);
}
}else if(mItem instanceof AvsPlayContentItem){
//cast our item for easy access
AvsPlayContentItem playItem = (AvsPlayContentItem) item;
try {
//set stream
getMediaPlayer().setAudioStreamType(AudioManager.STREAM_MUSIC);
//play new url
Log.e(TAG, "播放音频流2为:"+playItem.getUri());
getMediaPlayer().setDataSource(mContext, playItem.getUri());
} catch (IOException e) {
e.printStackTrace();
//bubble up our error
bubbleUpError(e);
} catch (IllegalStateException e){
e.printStackTrace();
//bubble up our error
bubbleUpError(e);
}
}else if(mItem instanceof AvsSpeakItem){
//cast our item for easy access
AvsSpeakItem playItem = (AvsSpeakItem) item;
//write out our raw audio data to a file
File path=new File(mContext.getCacheDir(), System.currentTimeMillis()+".mp3");
FileOutputStream fos = null;
try {
fos = new FileOutputStream(path);
fos.write(playItem.getAudio());
fos.close();
//play our newly-written file
Log.e(TAG, "播放音频流3的长度为:"+playItem.getAudio().length);
Log.e(TAG, "播放音频流3为:"+path.getPath().toString());
getMediaPlayer().setDataSource(path.getPath());
} catch (IOException|IllegalStateException e) {
e.printStackTrace();
//bubble up our error
bubbleUpError(e);
}
}
//prepare our player, this will start once prepared because of mPreparedListener
try {
getMediaPlayer().prepareAsync();
}catch (IllegalStateException e){
bubbleUpError(e);
}
}
以上就是AlexaAndroid关于Alexa的登录验证、语音收集、语音数据流的网络发送、网络响应数据的解析及播放的流程
参考文件:
1、https://github.com/liu475362194/AlexaAndroid-master
2、https://github.com/huangzhijin/AlexaAndroid-master
3、https://github.com/RAKWireless/WisCore/wiki/Login-Alexa-Through-App
Amazon Alexa登录授权
Alexa授权
亚马逊语言识别Alexa之AlexaAndroid的接入及AlexaAndroid的源码解析(一)