1111import android .os .AsyncTask ;
1212import android .util .Log ;
1313import io .flutter .util .PathUtils ;
14+ import org .json .JSONException ;
15+ import org .json .JSONObject ;
1416
1517import java .io .*;
1618import java .util .Collection ;
1719import java .util .HashSet ;
20+ import java .util .Scanner ;
1821import java .util .concurrent .CancellationException ;
1922import java .util .concurrent .ExecutionException ;
23+ import java .util .zip .CRC32 ;
24+ import java .util .zip .ZipEntry ;
25+ import java .util .zip .ZipException ;
26+ import java .util .zip .ZipFile ;
2027
2128/**
22- * A class to intialize the native code.
29+ * A class to initialize the native code.
2330 **/
2431class ResourceExtractor {
2532 private static final String TAG = "ResourceExtractor" ;
@@ -33,18 +40,46 @@ private class ExtractTask extends AsyncTask<Void, Void, Void> {
3340 private void extractResources () {
3441 final File dataDir = new File (PathUtils .getDataDirectory (mContext ));
3542
36- final String timestamp = checkTimestamp (dataDir );
43+ JSONObject updateManifest = readUpdateManifest ();
44+ if (!validateUpdateManifest (updateManifest )) {
45+ updateManifest = null ;
46+ }
47+
48+ final String timestamp = checkTimestamp (dataDir , updateManifest );
49+ if (timestamp == null ) {
50+ return ;
51+ }
52+
53+ deleteFiles ();
54+
55+ if (updateManifest != null ) {
56+ if (!extractUpdate (dataDir )) {
57+ return ;
58+ }
59+ }
60+
61+ if (!extractAPK (dataDir )) {
62+ return ;
63+ }
64+
3765 if (timestamp != null ) {
38- deleteFiles ();
66+ try {
67+ new File (dataDir , timestamp ).createNewFile ();
68+ } catch (IOException e ) {
69+ Log .w (TAG , "Failed to write resource timestamp" );
70+ }
3971 }
72+ }
4073
74+ /// Returns true if successfully unpacked APK resources,
75+ /// otherwise deletes all resources and returns false.
76+ private boolean extractAPK (File dataDir ) {
4177 final AssetManager manager = mContext .getResources ().getAssets ();
4278
4379 byte [] buffer = null ;
4480 for (String asset : mResources ) {
4581 try {
4682 final File output = new File (dataDir , asset );
47-
4883 if (output .exists ()) {
4984 continue ;
5085 }
@@ -62,28 +97,99 @@ private void extractResources() {
6297 while ((count = is .read (buffer , 0 , BUFFER_SIZE )) != -1 ) {
6398 os .write (buffer , 0 , count );
6499 }
100+
65101 os .flush ();
102+ Log .i (TAG , "Extracted baseline resource " + asset );
66103 }
67104 }
105+
68106 } catch (FileNotFoundException fnfe ) {
69107 continue ;
108+
70109 } catch (IOException ioe ) {
71110 Log .w (TAG , "Exception unpacking resources: " + ioe .getMessage ());
72111 deleteFiles ();
73- return ;
112+ return false ;
74113 }
75114 }
76115
77- if (timestamp != null ) {
78- try {
79- new File (dataDir , timestamp ).createNewFile ();
80- } catch (IOException e ) {
81- Log .w (TAG , "Failed to write resource timestamp" );
116+ return true ;
117+ }
118+
119+ /// Returns true if successfully unpacked update resources or if there is no update,
120+ /// otherwise deletes all resources and returns false.
121+ private boolean extractUpdate (File dataDir ) {
122+ if (FlutterMain .getUpdateInstallationPath () == null ) {
123+ return true ;
124+ }
125+
126+ final File updateFile = new File (FlutterMain .getUpdateInstallationPath ());
127+ if (!updateFile .exists ()) {
128+ return true ;
129+ }
130+
131+ ZipFile zipFile ;
132+ try {
133+ zipFile = new ZipFile (updateFile );
134+
135+ } catch (ZipException e ) {
136+ Log .w (TAG , "Exception unpacking resources: " + e .getMessage ());
137+ deleteFiles ();
138+ return false ;
139+
140+ } catch (IOException e ) {
141+ Log .w (TAG , "Exception unpacking resources: " + e .getMessage ());
142+ deleteFiles ();
143+ return false ;
144+ }
145+
146+ byte [] buffer = null ;
147+ for (String asset : mResources ) {
148+ ZipEntry entry = zipFile .getEntry (asset );
149+ if (entry == null ) {
150+ continue ;
151+ }
152+
153+ final File output = new File (dataDir , asset );
154+ if (output .exists ()) {
155+ continue ;
156+ }
157+ if (output .getParentFile () != null ) {
158+ output .getParentFile ().mkdirs ();
159+ }
160+
161+ try (InputStream is = zipFile .getInputStream (entry )) {
162+ try (OutputStream os = new FileOutputStream (output )) {
163+ if (buffer == null ) {
164+ buffer = new byte [BUFFER_SIZE ];
165+ }
166+
167+ int count = 0 ;
168+ while ((count = is .read (buffer , 0 , BUFFER_SIZE )) != -1 ) {
169+ os .write (buffer , 0 , count );
170+ }
171+
172+ os .flush ();
173+ Log .i (TAG , "Extracted override resource " + asset );
174+ }
175+
176+ } catch (FileNotFoundException fnfe ) {
177+ continue ;
178+
179+ } catch (IOException ioe ) {
180+ Log .w (TAG , "Exception unpacking resources: " + ioe .getMessage ());
181+ deleteFiles ();
182+ return false ;
82183 }
83184 }
185+
186+ return true ;
84187 }
85188
86- private String checkTimestamp (File dataDir ) {
189+ // Returns null if extracted resources are found and match the current APK version
190+ // and update version if any, otherwise returns the current APK and update version.
191+ private String checkTimestamp (File dataDir , JSONObject updateManifest ) {
192+
87193 PackageManager packageManager = mContext .getPackageManager ();
88194 PackageInfo packageInfo = null ;
89195
@@ -100,20 +206,119 @@ private String checkTimestamp(File dataDir) {
100206 String expectedTimestamp =
101207 TIMESTAMP_PREFIX + packageInfo .versionCode + "-" + packageInfo .lastUpdateTime ;
102208
209+ if (updateManifest != null ) {
210+ String baselineVersion = updateManifest .optString ("baselineVersion" , null );
211+ if (baselineVersion == null ) {
212+ Log .w (TAG , "Invalid update manifest: baselineVersion" );
213+ }
214+
215+ String updateVersion = updateManifest .optString ("updateVersion" , null );
216+ if (updateVersion == null ) {
217+ Log .w (TAG , "Invalid update manifest: updateVersion" );
218+ }
219+
220+ if (baselineVersion != null && updateVersion != null ) {
221+ if (!baselineVersion .equals (Integer .toString (packageInfo .versionCode ))) {
222+ Log .w (TAG , "Outdated update file for " + packageInfo .versionCode );
223+ } else {
224+ final File updateFile = new File (FlutterMain .getUpdateInstallationPath ());
225+ expectedTimestamp += "-" + updateVersion + "-" + updateFile .lastModified ();
226+ }
227+ }
228+ }
229+
103230 final String [] existingTimestamps = getExistingTimestamps (dataDir );
104231
105232 if (existingTimestamps == null ) {
106- return null ;
233+ Log .i (TAG , "No extracted resources found" );
234+ return expectedTimestamp ;
235+ }
236+
237+ if (existingTimestamps .length == 1 ) {
238+ Log .i (TAG , "Found extracted resources " + existingTimestamps [0 ]);
107239 }
108240
109241 if (existingTimestamps .length != 1
110242 || !expectedTimestamp .equals (existingTimestamps [0 ])) {
243+ Log .i (TAG , "Resource version mismatch " + expectedTimestamp );
111244 return expectedTimestamp ;
112245 }
113246
114247 return null ;
115248 }
116249
250+ /// Returns true if the downloaded update file was indeed built for this APK.
251+ private boolean validateUpdateManifest (JSONObject updateManifest ) {
252+ if (updateManifest == null ) {
253+ return false ;
254+ }
255+
256+ String baselineChecksum = updateManifest .optString ("baselineChecksum" , null );
257+ if (baselineChecksum == null ) {
258+ Log .w (TAG , "Invalid update manifest: baselineChecksum" );
259+ return false ;
260+ }
261+
262+ final AssetManager manager = mContext .getResources ().getAssets ();
263+ try (InputStream is = manager .open ("flutter_assets/isolate_snapshot_data" )) {
264+ CRC32 checksum = new CRC32 ();
265+
266+ int count = 0 ;
267+ byte [] buffer = new byte [BUFFER_SIZE ];
268+ while ((count = is .read (buffer , 0 , BUFFER_SIZE )) != -1 ) {
269+ checksum .update (buffer , 0 , count );
270+ }
271+
272+ if (!baselineChecksum .equals (String .valueOf (checksum .getValue ()))) {
273+ Log .w (TAG , "Mismatched update file for APK" );
274+ return false ;
275+ }
276+
277+ return true ;
278+
279+ } catch (IOException e ) {
280+ Log .w (TAG , "Could not read APK: " + e );
281+ return false ;
282+ }
283+ }
284+
285+ /// Returns null if no update manifest is found.
286+ private JSONObject readUpdateManifest () {
287+ if (FlutterMain .getUpdateInstallationPath () == null ) {
288+ return null ;
289+ }
290+
291+ File updateFile = new File (FlutterMain .getUpdateInstallationPath ());
292+ if (!updateFile .exists ()) {
293+ return null ;
294+ }
295+
296+ try {
297+ ZipFile zipFile = new ZipFile (updateFile );
298+ ZipEntry entry = zipFile .getEntry ("manifest.json" );
299+ if (entry == null ) {
300+ Log .w (TAG , "Invalid update file: " + updateFile );
301+ return null ;
302+ }
303+
304+ // Read and parse the entire JSON file as single operation.
305+ Scanner scanner = new Scanner (zipFile .getInputStream (entry ));
306+ return new JSONObject (scanner .useDelimiter ("\\ A" ).next ());
307+
308+ } catch (ZipException e ) {
309+ Log .w (TAG , "Invalid update file: " + e );
310+ return null ;
311+
312+ } catch (IOException e ) {
313+ Log .w (TAG , "Invalid update file: " + e );
314+ return null ;
315+
316+ } catch (JSONException e ) {
317+ Log .w (TAG , "Invalid update file: " + e );
318+ return null ;
319+ }
320+ }
321+
117322 @ Override
118323 protected Void doInBackground (Void ... unused ) {
119324 extractResources ();
0 commit comments