返回

解决Google Photos iOS集成难题:Library API迁移Picker API

IOS

Google Photos 从 Library API 迁移到 Picker API (iOS) 的难题及解决

最近在搞 Google Photos 集成,遇到个麻烦事。Google Photos 的 Library API 要在 3 月 31 号停用了,得换成 Picker API。

按 Google 官方文档说的,集成步骤挺清楚的:

  1. 搞到用户的 OAuth 2.0 访问令牌 (access token)。
  2. 创建一个会话 (session),这会返回一个包含 pickerUriPickingSession
  3. pickerUri 把用户导到 Google Photos App 里。

问题就卡在最后一步了。试了两种方法,都有问题:

尝试一:直接用 PickerUri 打开 Google Photos App

func openGooglePhotosApp(with pickerUri: String) {
        guard let url = URL(string: pickerUri) else {
            print("Invalid pickerUri")
            return
        }
        
        if UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        } else {
            print("Cannot open URL. Make sure the Google Photos app is installed.")
        }
    }

结果这代码直接打开了 Safari,跳到 pickerUri,然后,让输 Google 账号密码…… 我可不想让用户再输一遍。

尝试二:用 WKWebView 加载 PickerUri

private func loadGoogleWebViewToSelectPhotos(pickerUri: String) {
    guard let urlToLoad = URL(string: pickerUri) else {
        return
    }
    
    var request = URLRequest(url: urlToLoad)
    request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
    
    googleWebView.load(request)
}

还是一样,WebView 加载出来,又要输账号密码。这用户体验也太差了。

看起来这 Picker API 好像只考虑了 Web 应用,没怎么考虑 iOS。安卓那边好像挺简单的,用他们的 Photo Picker 组件就行了。

问题出在哪儿?

根本原因在于,Google Photos Picker API 的 pickerUri 设计主要是为了在浏览器环境中使用。它不是一个可以直接用来在 iOS 上调起 Google Photos App 并自动登录的 URL Scheme。

  • URL Scheme 问题 : 直接用 UIApplication.shared.open(url) 打开 pickerUri,系统不知道该怎么处理,所以默认用 Safari 打开。
  • 授权问题 : 即使用 WKWebView 加载,并且在请求头里加了 Authorization: Bearer \(accessToken),这个 token 是给 Picker API 用的,不是给 Google Photos Web 界面用的,所以还是会要求登录。

解决思路

既然官方的 Picker API 在 iOS 上直接用不行,那就只能曲线救国。我研究了一番,找到了几种可能的解决办法:

方案一:继续用 Library API (短期)

虽然官方说 Library API 要停用,但实际上不一定马上就完全不能用。可以先苟着,同时积极寻找其他方案。但这只是权宜之计,不能长期依赖。

操作: 啥也不用改,维持现状。

优点:
* 简单。
* 不需要立即做改动。

缺点:
* 有潜在风险, 该 API 可能随时彻底停止工作.

方案二: 反向代理(Proxy) + 网页内嵌选择

自己搭个反向代理服务器。客户端先把请求发到你的服务器,你的服务器带着用户的 access token 去请求 Picker API,获取到 pickerUri。然后服务器生成一个简单的 HTML 页面,这个页面里用 JavaScript 打开 pickerUri(因为 pickerUri 是设计给浏览器用的)。最后,客户端用 WKWebView 加载你服务器上的这个 HTML 页面。

原理:

  1. 客户端发送请求到你的反向代理服务器,带上用户的 access token。
  2. 反向代理服务器使用这个 access token 请求 Google Photos Picker API,获取 pickerUri
  3. 反向代理服务器构建一个简单的 HTML 页面:
    <!DOCTYPE html>
    <html>
    <head>
    
    </head>
    <body>
      <script>
        window.location.href = "[pickerUri]"; // 替换成实际的 pickerUri
      </script>
    </body>
    </html>
    
  4. 反向代理服务器把这个 HTML 页面返回给客户端。
  5. 客户端用 WKWebView 加载这个 HTML 页面。由于这个 HTML 页面会在浏览器环境中打开 pickerUri,所以能正常调出 Google Photos 的网页版选择器,并且由于是你的服务器去请求的 pickerUri,已经带上了正确的授权,所以不会再要求用户登录。

代码示例 (客户端, 假设反向代理服务器地址是 https://your-proxy-server.com/photos-picker):

func loadPhotosPicker() {
    guard let url = URL(string: "https://your-proxy-server.com/photos-picker?accessToken=\(accessToken)") else { // 将accessToken传递给你的代理
        return
    }
    let request = URLRequest(url: url)
    googleWebView.load(request)
}

代码示例(Node.js 简易反向代理服务器,仅做概念性展示,需要自己根据实际情况完善):

const express = require('express');
const axios = require('axios');
const app = express();

app.get('/photos-picker', async (req, res) => {
  const accessToken = req.query.accessToken;
  if (!accessToken) {
    return res.status(400).send('Missing access token');
  }

    try{
      // 1. Create the session by calling REST endpoint
      const sessionResponse =  await axios.post('https://photos.googleapis.com/v1/sessions',
       {},
       {
         headers: {
           'Authorization': `Bearer ${accessToken}`,
           'Content-Type': 'application/json',
         }
       });

       //2. Get the pickerURI
        const pickerUri = sessionResponse.data.pickerUri
        //3. Create HTML file with pickerUri
       const html = `
          <!DOCTYPE html>
          <html>
          <head>
            
          </head>
          <body>
            <script>
              window.location.href = "${pickerUri}";
            </script>
          </body>
          </html>
          `;
          res.send(html);

      }catch(ex){
        res.status(500).send(`Error creating the session: ${ex.message}`);
      }
});

app.listen(3000, () => {
  console.log('Proxy server listening on port 3000');
});

优点:

  • 能绕过 iOS 上直接使用 pickerUri 的问题。
  • 能保证用户不需要重复登录。

缺点:

  • 需要自己搭服务器,增加开发和维护成本。
  • 稍微复杂了点。
  • 用户体验可能比原生的略差。

安全建议:

  • 反向代理服务器必须做好安全防护,防止被攻击。
  • access token 要妥善保管,避免泄露。建议使用 HTTPS。
  • 通信需要做加密。

方案三:寻找第三方库 (如果存在)

可能有其他开发者已经遇到了同样的问题,并开发了相应的第三方库来解决。可以在 GitHub、CocoaPods 等地方搜索一下,看看有没有现成的解决方案。

优点:

  • 省事,不需要从头开发。

缺点:

  • 可能没有完全符合需求的库.
  • 需要评估第三方库的可靠性和安全性。

方案四:GoogleSignIn + Google Drive API + 自定义 Picker

如果你有使用 GoogleSignIn, 并有读取Google Drive的权限,那么就可以利用这个优势来获取用户的google photo:

  1. 通过GoogleSignIn获取access Token
  2. 调用 Google Drive API, 利用查询参数来检索在 Google Photos 的照片。比如q=mimeType contains 'image/' and 'me' in owners and trashed=false

优点:

  • 利用既有权限。

缺点:

  • 需要有 Google Drive API 的相关权限。
  • 不能使用原生的google photo picker

代码示例 (假设已有google access token):


func listGooglePhotos() {
        let url = URL(string: "https://www.googleapis.com/drive/v3/files?q=mimeType contains 'image/' and 'me' in owners and trashed=false&fields=files(id,name,thumbnailLink,webContentLink)")!
        
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
        
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
           //handle data...
        }
        task.resume()
}
  • thumbnailLink: 图片缩略图.
  • webContentLink: 直接下载链接.
  • 安全提示: 你必须申请https://www.googleapis.com/auth/drive.readonly or https://www.googleapis.com/auth/drive scope

方案五: 换用其他云存储服务 (下策)

如果实在搞不定 Google Photos 的集成,又不想自己搭服务器,可以考虑换用其他云存储服务,比如 Dropbox、OneDrive 等。这些服务可能提供了更方便的 iOS 集成方案。

优点:

  • 一劳永逸,避免了 Google Photos API 变更带来的麻烦。

缺点:

  • 可能需要迁移用户的数据,比较麻烦。
  • 用户可能不习惯用其他服务。

总结

就目前来看,方案二(反向代理 + 网页内嵌选择)或者方案四 (Google Drive API + 自定义 Picker) 可能是比较靠谱的。方案一虽然简单,但是不可持续。

这事儿确实挺坑的,Google 没给 iOS 开发者提供一个方便的 Picker API 集成方案,只能自己想办法绕。 希望 Google 以后能改进一下。