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 等 相关信息,都需要自己重更新填写。
这一个是可以直接运行的:
azizcse/AlexaAndroid
还有一个是下载运行测试过的:
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 () {
alexaManager = AlexaManager.getInstance(this , PRODUCT_ID);
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" );
}
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);
return APIKeyDecoder.decode(packageName, apiKey, context);
}
}
(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();
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;
}
}
public ThirdPartyResourceParser (Context context, String packageName) {
this ._packageName = packageName;
this ._context = context;
this ._apiKey = this .parseApiKey();
}
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());
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" ;
}
(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);
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;
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 {
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" />
<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){
mAuthorizationManager.checkLoggedIn(mContext, new ImplCheckLoggedInCallback() {
@Override
public void success (Boolean result) {
if (result) {
final String url = getEventsUrl();
TokenManager.getAccessToken(mAuthorizationManager.getAmazonAuthorizationManager(), mContext, new TokenManager.TokenCallback() {
@Override
public void onSuccess (final String token) {
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();
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 {
logIn(new ImplAuthorizationCallback<AvsResponse>(callback) {
@Override
public void onSuccess () {
sendAudioRequest(requestBody, callback);
}
});
}
}
});
}
public void logIn (@Nullable final AuthorizationCallback callback){
mAuthorizationManager.checkLoggedIn(mContext, new AsyncCallback<Boolean, Throwable>() {
@Override
public void start () {
}
@Override
public void success (Boolean result) {
if (result){
if (callback != null ){
callback.onSuccess();
}
}else {
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);
}
}
(7)最后的操作是在ExternalBrowserManager中的openUrl()用intent启动浏览器进行登录验证:
public void openUrl (AbstractRequest request, String url, Context context) throws AuthError {
CompatibilityUtil.assertCorrectManifestIntegration(context);
Intent intent = this .getIntent(url, context);
MAPLog.i(LOG_TAG, "Starting External Browser" );
try {
request.onStart();
context.startActivity(intent);
} 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." );
}
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){
String url = "https://api.amazon.com/auth/O2/token" ;
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 ){
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);
saveTokens(context, tokenResponse);
if (callback != null ){
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成功之后:
TokenManager.getAccessToken(mAuthorizationManager.getAmazonAuthorizationManager(), mContext, new TokenManager.TokenCallback() {
@Override
public void onSuccess (final String token) {
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();
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;
if (callback != null ){
callback.start();
}
Log.i(TAG, "Starting SpeechSendAudio procedure" );
start = System.currentTimeMillis();
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) {
mRequestBuilder.url(url);
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 ));
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())
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" );
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();
}
});
}
};
(2) handleResonse()对返回数据检查,保存在List数组中:
private void handleResponse (AvsResponse response) {
boolean checkAfter = (avsQueue.size() == 0 );
if (response != null ) {
for (int i = response.size() - 1 ; i >= 0 ; i--) {
if (response.get (i) instanceof AvsReplaceAllItem || response.get (i) instanceof AvsReplaceEnqueuedItem) {
avsQueue.clear();
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 (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) {
if (!audioPlayer.isPlaying()) {
audioPlayer.playItem((AvsPlayRemoteItem) current);
}
} else if (current instanceof AvsPlayContentItem) {
if (!audioPlayer.isPlaying()) {
audioPlayer.playItem((AvsPlayContentItem) current);
}
} else if (current instanceof AvsSpeakItem) {
if (!audioPlayer.isPlaying()) {
audioPlayer.playItem((AvsSpeakItem) current);
}
setState(STATE_SPEAKING);
} else if (current instanceof AvsStopItem) {
audioPlayer.stop();
avsQueue.remove(current);
} else if (current instanceof AvsReplaceAllItem) {
audioPlayer.stop();
avsQueue.remove(current);
} else if (current instanceof AvsReplaceEnqueuedItem) {
avsQueue.remove(current);
} else if (current instanceof AvsExpectSpeechItem) {
audioPlayer.stop();
avsQueue.clear();
startListening();
} else if (current instanceof AvsSetVolumeItem) {
setVolume(((AvsSetVolumeItem) current).getVolume());
avsQueue.remove(current);
} else if (current instanceof AvsAdjustVolumeItem) {
adjustVolume(((AvsAdjustVolumeItem) current).getAdjustment());
avsQueue.remove(current);
} else if (current instanceof AvsSetMuteItem) {
setMute(((AvsSetMuteItem) current).isMute());
avsQueue.remove(current);
} else if (current instanceof AvsMediaPlayCommandItem) {
sendMediaButton(this , KeyEvent.KEYCODE_MEDIA_PLAY);
Log.i(TAG, "Media play command issued" );
avsQueue.remove(current);
} else if (current instanceof AvsMediaPauseCommandItem) {
sendMediaButton(this , KeyEvent.KEYCODE_MEDIA_PAUSE);
Log.i(TAG, "Media pause command issued" );
avsQueue.remove(current);
} else if (current instanceof AvsMediaNextCommandItem) {
sendMediaButton(this , KeyEvent.KEYCODE_MEDIA_NEXT);
Log.i(TAG, "Media next command issued" );
avsQueue.remove(current);
} else if (current instanceof AvsMediaPreviousCommandItem) {
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()){
getMediaPlayer().stop();
}
getMediaPlayer().reset();
if (!TextUtils.isEmpty(mItem.getToken()) && mItem.getToken().contains("PausePrompt" )){
try {
AssetFileDescriptor afd = mContext.getAssets().openFd("shhh.mp3" );
getMediaPlayer().setDataSource(afd.getFileDescriptor(),afd.getStartOffset(),afd.getLength());
} catch (IOException e) {
e.printStackTrace();
bubbleUpError(e);
}
}else if (mItem instanceof AvsPlayRemoteItem){
AvsPlayRemoteItem playItem = (AvsPlayRemoteItem) item;
try {
getMediaPlayer().setAudioStreamType(AudioManager.STREAM_MUSIC);
Log.e(TAG, "播放音频流1为:" +playItem.getUrl());
getMediaPlayer().setDataSource(playItem.getUrl());
} catch (IOException e) {
e.printStackTrace();
bubbleUpError(e);
}
}else if (mItem instanceof AvsPlayContentItem){
AvsPlayContentItem playItem = (AvsPlayContentItem) item;
try {
getMediaPlayer().setAudioStreamType(AudioManager.STREAM_MUSIC);
Log.e(TAG, "播放音频流2为:" +playItem.getUri());
getMediaPlayer().setDataSource(mContext, playItem.getUri());
} catch (IOException e) {
e.printStackTrace();
bubbleUpError(e);
} catch (IllegalStateException e){
e.printStackTrace();
bubbleUpError(e);
}
}else if (mItem instanceof AvsSpeakItem){
AvsSpeakItem playItem = (AvsSpeakItem) item;
File path=new File(mContext.getCacheDir(), System.currentTimeMillis()+".mp3" );
FileOutputStream fos = null ;
try {
fos = new FileOutputStream(path);
fos.write(playItem.getAudio());
fos.close();
Log.e(TAG, "播放音频流3的长度为:" +playItem.getAudio().length);
Log.e(TAG, "播放音频流3为:" +path.getPath().toString());
getMediaPlayer().setDataSource(path.getPath());
} catch (IOException|IllegalStateException e) {
e.printStackTrace();
bubbleUpError(e);
}
}
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登录授权
https://www.cnblogs.com/dixonyy/p/6767800.html
Alexa授权
https://blog.csdn.net/u012382509/article/details/61203292
亚马逊语言识别Alexa之AlexaAndroid的接入及AlexaAndroid的源码解析(一)
https://blog.csdn.net/wangyongyao1989/article/details/80183020