Ryan Cheng Android Develop Blog

翻译:Google IO 同步策略

翻译了一下google IO的同步策略和数据格式,对于要做数据同步的应用应该有些指导意义。

Copyright 2014 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

同步协议和数据格式

对于会议数据从服务器同步的方式,我们引入了一个主要的改变: IOSched现在仅从一个配置文件中指定的URL加载JSON格式数据,而不是去请求指定的后台。

同步过程开始于发起一个加载manifest文件的HTTP GET请求。URL 由Config.java文件中的MANIFEST_URL常量配置。

在Google I/O中,我们使用了Google Cloud Storage作为云服务器来存储这些JSON文件,然而,如果你处理自己的事件,你没必要一定要用Google Cloud Storage,任何可靠的云服务皆可。

IOSched在对manifest文件的请求中发送一个If-Modified-Since HTTP header,指定了该文件上一次下载的时间戳。如果服务器返回HTTP 状态码304 (无修改),IOSched则认为同步完成,结束操作,因为manifest文件没有更新。如果服务器返回200 OK,下一步就解析manifest数据。下面是manifest文件内容的示例:

{
    "format": "iosched-json-v1",
    "data_files": [
        "past_io_videolibrary_v5.json",
        "experts_v11.json",
        "hashtags_v8.json",
        "blocks_v10.json",
        "map_v11.json",
        "keynote_v10.json",
        "partners_v2.json",
        "session_data_v2.681.json"
    ]
}

注意到data_files数组中的每一个条目实际上是指向另外一个JSON文件,分别包含每一种条目的具体信息。它们的名字并无特别之处,可以随意指定。我们的做法是添加版本计划作后缀 (_v<N>.json) 。

注意: 这些文件是通过名字缓存在app里,因此如果app在某时刻已经下载了keynote_v10.json,就不会再次下载它。因此,如果你用于自己的事件,每次你修改了一个文件,要确保同步修改它的名字。比如,重命名为keynote_v11.json(同时更新manifest)。简而言之, 一旦发布,每个文件即视为不可修改

所以通常更新一个或多个文件的步骤是:

  1. 下载 foov1.json,修改,上传 foov2.json
  2. 下载 barv7.json,修改,上传 barv8.json
  3. 下载 manifest.json,改为指向 foov2 和 barv8,上传

注意顺序:你应该始终最后更新manifest文件,因为一旦你上传了它,它指向的文件就对用户可见了。

每一个JSON文件的格式是:

    {
       "<entity_collection>": [
           <entity>,
           <entity>,
           <entity>
       ],
       "<entity_collection>": [
           <entity>,
           <entity>,
           <entity>
       ]
    }

每个文件由一个或多个条目的集合组成,根据类型组织起来。示例的条目集合有:sessions,speakers,rooms等。

在你写这些文件的时候,哪个集合存于哪个文件完全由你决定。比如,在我们这,我们把sessions,speakers和rooms的条目一起放在sessiondatavN.json中,当然我们也可以放到3个JSON文件中。

这是一个包含room,session和speaker的例子:

{
  "rooms": [
    {
        "id": "ROOM1",
        "name": "Room Alpha"
    }
  ],
  "sessions": [
    {
        "id": "SESSION1"
        "description": "A cool session about example data.",
        "title": "Example Data in Action",
        "url": "http://www.example.com",
        "tags": [
            "TYPE_SESSION",
            "TOPIC_ANDROID",
            "TOPIC_CHROME",
            "THEME_DEVELOP",
            "THEME_DESIGN"
        ]
        "startTimestamp": "2014-06-26T22:00:00Z",
        "endTimestamp": "2014-06-26T22:30:00Z",
        "youtubeUrl": "dQw4w9WgXcQ",
        "speakers": [
            "SPEAKER1"
        ],
        "room": "ROOM1",
        "isLivestream": true,
        "captionsUrl": "http://......"
    }
  ],
  "speakers": [
    {
        "id": "SPEAKER1",
        "name": "Example Smith",
        "bio": "Mr. Example Smith is a great speaker.",
        "plusoneUrl":  
            "https://plus.google.com/12345677890123456789012",
        "thumbnailUrl":
            "https://example.com/..."
    }
  ]
}

注意到3个集合("rooms","speakers" 和 "sessions")在一个文件中,如果放在3个文件中同样有效。

重要:如果不止一个文件指定了同种条目的集合,IOSched会把这些集合联合起来。也就是说如果你有多个文件指定了sessions,它们最终会形成同一个sessions集合。

Bootstrap 数据

当用户首次运行app的时候,它们期望看到数据。然而,如果我们仅仅依赖同步机制给app带来数据,用户首次使用时将会盯着白屏等待同步,这是一个不好的用户体验。

这就是为什么IOSched使用预加载的“bootstrap 数据”过渡,本质上是一个预加载的JSON数据离线快照。这份数据在app首次运行的时候会被解析并保存到数据库。

这份文件在这 res/raw/bootstrap.json。仅仅是一个包含服务器上的JSON文件联合快照的文本文件。

数据格式

下面是IOSched支持的每一个条目类型的格式文档。

Rooms

Rooms 是 sessions 发生的地方。

{
  "rooms": [
    <room>,
    <room>,
    <room>,
    ...
  ]
}

每一个 <room> 的格式: JSON { "id": "ROOM1", "name": "Room Alpha" }

Blocks

Blocks是sessions或其他有趣的活动发生的时间段。Blocks仅仅在My Schedule页显示,告诉用户哪些是该event的主要blocks。比如,早餐和午餐就是blocks。

{
  "blocks": [
    <block>,
    <block>,
    <block>,
    ...
  ]
}

表示一个进餐中断的<block>例子:

{
    "title": "Lunch",
    "subtitle": "Cafe, Level 1", 
    "type": "break", 
    "start": "2014-06-25T18:30:00.000Z",
    "end": "2014-06-25T20:00:00.000Z"
}

表示一个自由中断的<block>例子:

{
    "title": "",
    "type": "free",
    "subtitle": "",
    "start": "2014-06-25T18:00:00.000Z",
    "end": "2014-06-25T19:00:00.000Z"
}

这种block类型是free因为它在app的My Schedule中显示为自由中断,允许用户点击并且查看在该block的时间段内开始的sessions。每一个session的起始时间应该位于至少一个自由中断的时间段内,否则用户无法在My Schedule中添加到自己的日程。 重叠的 free blocks 在Android app中处理。

Sessions

{
  "sessions": [
    <session>,
    <session>,
    <session>,
    ...
  ]
}

每一个 <session> 的格式:

{
   "id": "SESSION123"
   "url": "https://...."
   "title": "Web Components in Action",
   "description": "Web components are cool.",
   "tags": [
       "TYPE_SESSION",
       "TOPIC_ANDROID",
       "TOPIC_CHROME",
       "THEME_DEVELOP",
       "THEME_DESIGN"
   ]
   "mainTag": "TOPIC_ANDROID",
   "startTimestamp": "2014-06-25T22:10:00Z",
   "endTimestamp": "2014-06-25T22:55:00Z"
   "photoUrl": "https://...../photo.jpg",
   "youtubeUrl": "https://youtu.be/YCUZ01yFtsM",
   "speakers": [
       "SPEAKER123",
       "SPEAKER456"
   ],
   "room": "ROOM123",
   "isLivestream": true,
   "captionsUrl": "http://......",
   "color": "#607d8b",
   "hashtag": "webcomponents"
}

session中的URL是session的web链接,也是用作用户对session +1的URL。session的标签不是随意的,必须定义在 "tags" 集合中。start end时间戳必须是这种格式,使用UTC时区。photo的URL 是session的插图的URL。captions URL是session直播的插图URL。如果不需要,设为 "" 。 Color是session详情中显示的品牌颜色,hashtag是当用户在session详情中点击社交按钮时自动添加 Google+标签。

如果 isLivestream 设为 true,session 表示直播, 同时 youtubeUrl 指定在 Youtube 上直播的URL。如果session不是直播,youtubeUrl指定session的视频URL,如果存在的话。

Speakers

speaker 是 session 的主持人。每一个session可以有0个或多个主持人。有主持人的Sessions更加有趣。

{
  "speakers": [
    <speaker>,
    <speaker>,
    <speaker>,
    ...
  ]
}

speaker示例:

{
    "id": "SPEAKER123",
    "name": "Reto Meier",
    "bio": "Reto is the lead of the Scalable Developer Advocacy team",
    "company": "Google",
    "thumbnailUrl": "http://..../reto.jpg",
    "plusoneUrl": "https://plus.google.com/+RetoMeier"
}

Tags

Tags 就是 tags。 用来对sessions归类。在IOSched,一个session可以被标签为TOPIC, THEME和TYPE。TOPIC tags例子有: "Android", "Chrome", 等等。THEME tags例子有: "Design", "Develop", "Distribute"。TYPE tags 例子有: "Session", "Codelab", "Office hours",等等。数据中的 "tags" 条目指定了所有可能的tags和categories。

{
  "tags": [
    <tag>,
    <tag>,
    <tag>,
    ...
  ]
}

"Android" 主题的 tag示例:

{
    "category": "TOPIC",
    "tag": "TOPIC_ANDROID",
    "name": "Android",
    "abstract": "",
    "order_in_category": 1,
    "color": "#558b2f"
}

注意到我们将该 tag b归类为 "TOPIC" 类别,也就是说这个 tag 指定了session的topic。

表示 "Develop" 主题的 tag 示例:

{
    "category": "THEME",
    "tag": "THEME_DEVELOP",
    "name": "Distribute",
    "abstract": "",
    "order_in_category": 2
}

最后,这是一个 表示"Office Hours" 类型的 session tag:

{
    "category": "TYPE",
    "tag": "TYPE_OFFICEHOURS",
    "name": "Office Hours",
    "abstract": "",
    "order_in_category": 6
}

orderincategory 参数用于tags列表中的排序。所以,"Office Hours"将会显示在 orderincategory 小于 6 的tag后面, orderincategory 大于 6 的 tag 前面。

Experts

Experts 表示google开发专家,在app中显示在 "Experts" 页。 JSON { "experts": [ <expert>, <expert>, <expert>, ... ] }

expert示例:

{
    "id": "EXPERT123",
    "name": "Bruno Oliveira", 
    "attending": true, 
    "bio": "Lorem ipsum dolor sit amet", 
    "city": "Sao Paulo, SP", 
    "country": "Brazil", 
    "imageUrl": "https://....../photo.jpg",
    "plusId": "+BrunoOliveira",
    "url": "https://plus.google.com/+BrunoOliveira"
} 

Hashtags

Hashtags 是出现在 "Social" 页的标签。

{
  "hashtags": [
    <hashtag>,
    <hashtag>,
    <hashtag>,
    ....
  ]
}

hashtag例子:

{
    "color": "#ff8a65",
    "description": "Experience the magic of I/O remotely",
    "name": "#io14extended",
    "order": 11
}

Partners

Partners 是map中列出的合作伙伴公司。

{
  "partners": [
    <partner>,
    <partner>,
    <partner>,
    ...
  ]
}

partner例子:

{
    "id": "PARTNER123", 
    "name": "Example Corp", 
    "website": "http://www.example.com/", 
    "logo": "http://.../example.png",
    "desc": "Example Corp produces great example material."
} 

Videos

Videos 是视频库中列出的 Youtube 内容链接。

{
  "videos": [
    <video>,
    <video>,
    <video>,
    ...
  ]
}

video例子:

{
    "year": "2013",
    "title": "What's New in Android Developer Tools",
    "desc": "A summary of new features for Android developers",
    "vid": "lmv1dTnhLH4",
    "id": "lmv1dTnhLH4",
    "thumbnailUrl": "http://img.youtube.com/vi/lmv1dTnhLH4/hqdefault.jpg",
    "topic": "Android",
    "speakers": "Xavier Ducrohet, Tor Norbye"
}

vid 是 Youtube 视频的 ID。

注意:由于Android app的一个bug,"id" 必须跟 "vid"一致。

Map

map 数据跟其它的数据不一样,因为它不是条目的集合。它仅仅是一个 JSON 对象,格式如下:

{
  "map": [
  {
     "config": {
         "enableMyLocation": "false"
     }, 
     "markers": {
         "0": [
             {
                  "id": "ROOM8", 
                  "lat": 37.78280631538293, 
                  "lng": -122.40401487797499, 
                  "title": "Room 8", 
                  "type": "session"
             }, 
             ....
         ],
         "1": [
             //...more markers...
         ],
         "2": [
             //...more markers...
         ],
     },
     "tiles": {
         "0": {
             "filename": "floor2-2014-v1.svg", 
             "url": ""
         }, 
         "1": {
             "filename": "floor1-2014-v1.svg", 
             "url": ""
         }, 
         "2": {
             "filename": "floor0-2014-v1.svg", 
             "url": ""
         }
     }
  }
  ]
}

enableMyLocation 表示在室内地图上是否开启室内定位。 markers 数组中的每一个marker地图上的一个标记,拥有ID,经度,纬度,标题和类型。Markers根据每一层分类 (通过"0", "1" 和 "2" 关键字)。

使用这种marker格式指定一个room:

{
     "id": "ROOM8", 
     "lat": 37.78280631538293, 
     "lng": -122.40401487797499, 
     "title": "Room 8", 
     "type": "session"
}

不想添加一个标记而给map加个label,把 "type" 值设为 "label" 即可:

{
    "id": "gearpickup", 
    "lat": 37.78331825838168, 
    "lng": -122.40340635180475, 
    "title": "Gear Pickup", 
    "type": "label"
}

想要放一个合作伙伴的标记 (点击会打开合作伙伴列表), 使用 "partnerlabel" 类型:

{
    "id": "partnersandboxlabel", 
    "lat": 37.78325148340904, 
    "lng": -122.40336142480373, 
    "title": "Partner\nSandbox", 
    "type": "partnerlabel"
}

"tiles" 指定用作每一楼层的地图覆盖层的 SVG 文件。可选的 "url" 参数指定文件下载地址,以应对文件没有预装在app中的情况。