java - 身份验证后带有 gmail-api 的 UserRecoverableAuthIOException
问题描述
我正在使用Gmail api,许多用户抱怨发送电子邮件不起作用。对于大多数用户来说,它工作正常,我无法重现该问题。在 Firebase 中,我收到以下崩溃报告。
Non-fatal Exception: com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
at com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential$RequestHandler.intercept(GoogleAccountCredential.java:297)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:868)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:419)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:352)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:469)
at com.dummydomain.myapp.EmailUtils.sendMessage(EmailUtils.java:397)
(...)
Caused by com.google.android.gms.auth.d: NeedPermission
at com.google.android.gms.auth.zze.zzb(zze.java:13)
at com.google.android.gms.auth.zzd.zza(zzd.java:77)
at com.google.android.gms.auth.zzd.zzb(zzd.java:20)
at com.google.android.gms.auth.zzd.getToken(zzd.java:7)
at com.google.android.gms.auth.zzd.getToken(zzd.java:5)
at com.google.android.gms.auth.zzd.getToken(zzd.java:2)
at com.google.android.gms.auth.GoogleAuthUtil.getToken(GoogleAuthUtil.java:55)
at com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential.getToken(GoogleAccountCredential.java:267)
at com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential$RequestHandler.intercept(GoogleAccountCredential.java:292)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:868)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:419)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:352)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:469)
at com.dummydomain.myapp.EmailUtils.sendMessage(EmailUtils.java:397)
(...)
以下是我的身份验证过程的要点。此时已授予 Manifest.permission.GET_ACCOUNTS,并且 Google Cloud Console 中的相应 API 内容已正确配置,并且我的应用已验证使用敏感权限/范围 GMAIL_SEND。
private void authenticate() {
String[] SCOPES = {GmailScopes.GMAIL_SEND};
GoogleAccountCredential mCredential = GoogleAccountCredential.usingOAuth2(
context,
Arrays.asList(SCOPES))
.setBackOff(new ExponentialBackOff());
startActivityForResult(mCredential.newChooseAccountIntent(),REQUEST_ACCOUNT_PICKER);
}
// which returns in
public void onActivityResult(...) {
// (...)
switch(requestCode) {
case REQUEST_ACCOUNT_PICKER:
if (resultCode == Activity.RESULT_OK && data != null && data.getExtras() != null) {
String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
if (accountName != null) {
// Check if not a gmail account, since gmail api only works with that gmail accounts...
if(!accountName.contains("@gmail.com") && !accountName.contains("@googlemail.com")){
// --> tell user to select a google account
return;
}
mCredential.setSelectedAccount(new Account(accountName, BuildConfig.APPLICATION_ID));
// Got account, now test if we have access
new CheckAccessTask().execute();
}else{
// (...)
}
}
break;
case REQUEST_AUTHORIZATION:
if (resultCode != Activity.RESULT_OK) {
// choose new account
startActivityForResult(mCredential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
}else{
// Got authorization, so test email
new CheckAccessTask().execute();
}
break;
}
}
private class CheckAccessTask extends AsyncTask<Void, Void, Boolean> {
private Exception mLastError = null;
@Override
protected Boolean doInBackground(Void... params) {
try {
// Check if we got token - will crash with UserRecoverableAuthException
// if user didn't accept the google consent screen
mCredential.getToken();
// access granted, return true
return true;
} catch (Exception e) {
mLastError = e;
cancel(true);
return false;
}
}
@Override
protected void onCancelled() {
if (mLastError != null) {
if (mLastError instanceof GooglePlayServicesAvailabilityIOException) {
// Play Services not found --> cancel activation
} else if (mLastError instanceof UserRecoverableAuthException) {
startActivityForResult(((UserRecoverableAuthException) mLastError).getIntent(), REQUEST_AUTHORIZATION);
} else if (mLastError instanceof UserRecoverableAuthIOException) {
startActivityForResult(((UserRecoverableAuthIOException) mLastError).getIntent(), REQUEST_AUTHORIZATION);
} else {
// Other error --> cancel activation
}
} else {
// --> cancel activation
}
}
@Override
protected void onPostExecute(Boolean accessGranted) {
if (accessGranted){
// SUCCESS! Save email in shared preferences for later use
String accountName = mCredential.getSelectedAccountName();
prefs.putString(Constants.SENDER_ACCOUNT, accountName);
}
}
}
发送电子邮件过程的要点(在实际代码中电子邮件是 HTML)
private void sendEmail(){
GoogleAccountCredential mCredential = GoogleAccountCredential.usingOAuth2(
context,
Arrays.asList(SCOPES))
.setBackOff(new ExponentialBackOff());
// set sender account
String senderAccount = prefs.getString(Constants.SENDER_ACCOUNT, null);
if(senderAccount == null) return; // Authentication not complete
mCredential.setSelectedAccount(new Account(senderAccount, BuildConfig.APPLICATION_ID));
// Initialize service object
HttpTransport transport = AndroidHttp.newCompatibleTransport();
JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
Gmail mGmailApiService = new Gmail.Builder(
transport, jsonFactory, mCredential)
.setApplicationName("My-App")
.build();
// Construct email
com.google.api.services.gmail.model.Message message;
try {
// create MimeMessage
Properties props = new Properties();
Session session = Session.getDefaultInstance(props, null);
MimeMessage mimeMessage = new MimeMessage(session);
mimeMessage.setSubject("Test email");
mimeMessage.setText("Hello this is an email sent from android");
mimeMessage.setFrom(new InternetAddress(mCredential.getSelectedAccountName()));
mimeMessage.setRecipients(javax.mail.Message.RecipientType.TO, InternetAddress.parse(senderAccount)); // send to yourself
// Convert MimeMessage to Message
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
mimeMessage.writeTo(bytes);
String encodedEmail = Base64.encodeBase64URLSafeString(bytes.toByteArray());
message = new com.google.api.services.gmail.model.Message();
message.setRaw(encodedEmail);
} catch (MessagingException | IOException e) {
e.printStackTrace();
return;
}
// Send email
try {
mGmailApiService.users().messages().send("me", message).execute(); // (EmailUtils:397)
// Success, email sent!
} catch (IOException e) {
e.printStackTrace();
// ==== THIS is where users get UserRecoverableAuthIOExceptions ==== //
}
}
通过firebase日志记录,我发现崩溃发生在身份验证后不久和很久之后。我还可以看到,有时代码会成功发送一封电子邮件,然后在下一封电子邮件之后立即崩溃。我也收到不少报告SocketTimeoutException
,我认为这些报告是由互联网连接缓慢/故障引起的。
感谢您的时间。
编辑: 我能够重现错误的唯一方法是从“具有帐户访问权限的第三方应用程序”列表中手动删除我的应用程序。但我不明白为什么在没有用户交互的情况下会发生这种情况。
解决方案
推荐阅读
- javascript - 快递:BasicAuth
- r - 计算 Spearman 的 rho 以增加 for 循环中的行子集
- swift4 - 无法将类型 '(Any) -> ()' 的值转换为预期的参数类型 '(_) -> _'
- python - 深拷贝特定属性
- regex - 替换匹配组中字符的多个实例
- python - 从文件获取输入时子进程python模块问题
- javascript - Electron dialog.showOpenDialog() 过滤器不工作
- batch-file - 删除不需要的目录,在屏幕上强制产生错误
- javascript - Twitter Bootstrap:导航栏未在移动视图中打开
- java - 在Android中绘制动态自定义视图棋盘