Android使用Gmail API报错:unable to create directory: /tokens
解决Android端调用Gmail API时的/tokens目录创建失败问题
问题描述
使用Google Gmail API Java快速启动示例代码在Android平台上调用Gmail API发送邮件(不通过Intent),已配置Android端OAuth 2.0 Client ID,但运行时触发致命异常:
FATAL EXCEPTION: main Process: com.nls.myapplication22, PID: 3216 java.lang.RuntimeException: Unable to start activity ComponentInfo: java.lang.RuntimeException: java.io.IOException: unable to create directory: /tokens
问题代码分析
原代码直接沿用桌面端实现,存在两个核心问题:
- 存储路径错误:使用相对路径
"tokens"会尝试在系统根目录创建文件夹,Android应用无此权限。 - 授权组件不兼容:
LocalServerReceiver是桌面端用于接收OAuth回调的组件,Android平台无法启动本地监听端口。
原核心代码片段:
String TOKENS_DIRECTORY_PATH = "tokens"; [...] GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES) .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIRECTORY_PATH))) .setAccessType("offline") .build(); LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build(); Credential credential = new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
解决方案
1. 调整存储方式(解决目录创建权限问题)
替换桌面端的FileDataStoreFactory为Android专用的SharedPreferencesDataStoreFactory,利用应用内部存储保存OAuth令牌,无需额外权限:
// 获取应用SharedPreferences实例 SharedPreferences sharedPreferences = getApplicationContext() .getSharedPreferences("GmailOAuthTokens", Context.MODE_PRIVATE); // 创建适配Android的DataStoreFactory DataStoreFactory dataStoreFactory = new SharedPreferencesDataStoreFactory(sharedPreferences); // 重构AuthorizationCodeFlow GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES) .setDataStoreFactory(dataStoreFactory) .setAccessType("offline") .build();
2. 替换授权流程(适配Android平台)
移除LocalServerReceiver和AuthorizationCodeInstalledApp,改用Google官方的Android登录组件GoogleSignInClient完成OAuth授权:
第一步:修改Gradle依赖
替换桌面端Jetty库为Android适配的OAuth组件和登录库:
dependencies { [...] implementation("com.google.api-client:google-api-client:2.2.0") // 移除桌面端Jetty依赖 // implementation("com.google.oauth-client:google-oauth-client-jetty:1.34.1") // 添加Android OAuth客户端库 implementation("com.google.oauth-client:google-oauth-client-android:1.34.1") implementation("com.google.apis:google-api-services-gmail:v1-rev20220404-2.0.0") implementation("javax.mail:mail:1.4.7") // 添加Google登录组件 implementation("com.google.android.gms:play-services-auth:20.7.0") }
第二步:实现Google登录授权流程
// 定义登录请求码 private static final int RC_SIGN_IN = 1001; private GoogleSignInClient mGoogleSignInClient; // 初始化GoogleSignInClient private void initGoogleSignIn() { GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestScopes(new Scope(GmailScopes.GMAIL_SEND)) // 请求Gmail发送权限 .requestEmail() .build(); mGoogleSignInClient = GoogleSignIn.getClient(this, gso); } // 启动登录流程 private void startSignIn() { Intent signInIntent = mGoogleSignInClient.getSignInIntent(); startActivityForResult(signInIntent, RC_SIGN_IN); } // 处理登录回调 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == RC_SIGN_IN) { Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data); handleSignInResult(task); } } // 处理登录结果,获取用于Gmail API的Credential private void handleSignInResult(Task<GoogleSignInAccount> completedTask) { try { GoogleSignInAccount account = completedTask.getResult(ApiException.class); // 获取访问令牌 String accessToken = GoogleAuthUtil.getToken(this, account.getAccount(), "oauth2:" + GmailScopes.GMAIL_SEND); // 构建Credential Credential credential = new Credential.Builder(BearerToken.authorizationHeaderAccessMethod()) .setAccessToken(accessToken) .setRefreshToken(account.getRefreshToken()) .build(); // 初始化Gmail服务 Gmail gmailService = new Gmail.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential) .setApplicationName("你的应用名称") .build(); // 调用Gmail API发送邮件 sendEmail(gmailService); } catch (ApiException | IOException e) { e.printStackTrace(); } } // 发送邮件示例方法(需实现邮件构建逻辑) private void sendEmail(Gmail service) throws MessagingException, IOException { MimeMessage mimeMessage = createEmail("收件人邮箱", "发件人邮箱", "邮件主题", "邮件内容"); Message message = createMessageWithEmail(mimeMessage); service.users().messages().send("me", message).execute(); }
3. 补充权限声明
在AndroidManifest.xml中添加网络权限:
<uses-permission android:name="android.permission.INTERNET" />
问题根源总结
- 权限问题:桌面端代码默认在根目录创建令牌存储文件夹,Android应用无系统根目录写入权限。
- 组件不兼容:
LocalServerReceiver是桌面端专属组件,Android无法启动本地监听端口接收OAuth回调。 - 依赖错误:引入了桌面端Jetty服务器依赖,Android平台无需该组件。
内容的提问来源于stack exchange,提问作者averagestudent
相关产品推荐
相关产品推荐

