首页 > 解决方案 > 身份验证后带有 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,我认为这些报告是由互联网连接缓慢/故障引起的。

感谢您的时间。

编辑: 我能够重现错误的唯一方法是从“具有帐户访问权限的第三方应用程序”列表中手动删除我的应用程序。但我不明白为什么在没有用户交互的情况下会发生这种情况。

标签: javaandroidgmail-api

解决方案


推荐阅读