diff --git a/README.md b/README.md
index 1a184f2..b471bc4 100644
--- a/README.md
+++ b/README.md
@@ -100,6 +100,31 @@ to generate your own certificates and do the same.
+Android (requires a rooted device)
+
+* Install Zwift on the device
+* Append the contents of ``ssl/cert-zwift-com.pem`` to ``/data/data/com.zwift.zwiftgame/dataES/cacerts.pem`` on the device
+ * Note: this file will only exist after the first run of Zwift since it's downloaded after the initial install
+ * Simple approach to achieve this if your device doesn't have a text editor:
+ * ``adb push ssl/cert-zwift-com.pem /data/data/com.zwift.zwiftgame/dataES/``
+ * In ``adb shell``: ``cd /data/data/com.zwift.zwiftgame/dataES/``
+ * In ``adb shell``: ``cat cert-zwift-com.pem >> cacerts.pem``
+ * However you do it, ensure the permissions and ownership of the file remains the same.
+* Modify the device's /etc/hosts file
+ * Append this line: `` us-or-rly101.zwift.com secure.zwift.com cdn.zwift.com``
+
(Where ```` is the ip address of the machine running zoffline. If
+ it's running on the same machine as Zwift, use ``127.0.0.1`` as the ip.)
+ * If no text editor on the device, recommend:
+ * ``adb pull /etc/hosts``
+ * (modify on PC)
+ * ``adb push hosts /etc/hosts``
+* Start Zwift and sign in using any email/password
+
+Why: We need to redirect Zwift to use zoffline and convince Zwift to
+accept zoffline's self signed certificates for Zwift's domain names. Feel free
+to generate your own certificates and do the same.
+
+
#### Enabling/Disabling zoffline
diff --git a/zwift_offline.py b/zwift_offline.py
index a3328fa..cae5114 100644
--- a/zwift_offline.py
+++ b/zwift_offline.py
@@ -24,7 +24,8 @@ import protobuf.profile_pb2 as profile_pb2
import protobuf.segment_result_pb2 as segment_result_pb2
import protobuf.world_pb2 as world_pb2
-app = Flask(__name__)
+# Android uses https for cdn
+app = Flask(__name__, static_folder='cdn/gameassets', static_url_path='/gameassets')
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
STORAGE_DIR = "%s/storage" % SCRIPT_DIR
@@ -337,29 +338,47 @@ def api_profiles_goals_id(player_id, goal_id):
return '', 200
+def relay_worlds_generic(world_id=None):
+ # Android client also requests a JSON version
+ if request.headers['Accept'] == 'application/json':
+ world = { 'currentDateTime': int(time.time()),
+ 'currentWorldTime': int(time.time())*1000,
+ 'friendsInWorld': [],
+ 'mapId': 1,
+ 'name': 'Public Watopia',
+ 'playerCount': 0,
+ 'worldId': 1
+ }
+ if world_id:
+ world['mapId'] = world_id
+ return jsonify(world)
+ else:
+ return jsonify([ world ])
+ else: # protobuf request
+ worlds = world_pb2.Worlds()
+ world = worlds.worlds.add()
+ world.id = 1
+ world.name = 'Public Watopia'
+ world.f3 = 1
+ # Windows client crashes if playerCount is 0
+ world.f5 = 1 # playerCount
+ world.world_time = int(time.time())*1000
+ world.real_time = int(time.time())
+ if world_id:
+ world.id = world_id
+ return worlds.SerializeToString()
+ else:
+ return world.SerializeToString()
+
+
@app.route('/relay/worlds', methods=['GET'])
def relay_worlds():
- worlds = world_pb2.Worlds()
- world = worlds.worlds.add()
- world.id = 1
- world.name = 'Public Watopia'
- world.f3 = 1
- world.f5 = 1
- world.world_time = int(time.time())*1000
- world.real_time = int(time.time())
- return worlds.SerializeToString(), 200
+ return relay_worlds_generic()
@app.route('/relay/worlds/', methods=['GET'])
def relay_worlds_id(world_id):
- # XXX: Will need to keep MapSchedule.xml up to date
- return jsonify({ 'currentDateTime': int(time.time()),
- 'currentWorldTime': int(time.time())*1000,
- 'friendsInWorld': [ ],
- 'mapId': world_id,
- 'name': 'Public Watopia',
- 'playerCount': 0,
- 'worldId': 1 })
+ return relay_worlds_generic(world_id)
@app.route('/relay/worlds//join', methods=['POST'])