• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package io.flutter.view;
6 
7 import static java.util.Arrays.asList;
8 
9 import android.content.Context;
10 import android.content.pm.PackageInfo;
11 import android.content.pm.PackageManager;
12 import android.content.res.AssetManager;
13 import android.os.AsyncTask;
14 import android.os.Build;
15 import android.support.annotation.NonNull;
16 import android.support.annotation.WorkerThread;
17 import android.util.Log;
18 
19 import io.flutter.BuildConfig;
20 import io.flutter.util.PathUtils;
21 
22 import org.json.JSONObject;
23 
24 import java.io.*;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.HashSet;
28 import java.util.concurrent.CancellationException;
29 import java.util.concurrent.ExecutionException;
30 import java.util.zip.GZIPInputStream;
31 import java.util.zip.ZipEntry;
32 import java.util.zip.ZipFile;
33 
34 /**
35  * A class to initialize the native code.
36  **/
37 class ResourceExtractor {
38     private static final String TAG = "ResourceExtractor";
39     private static final String TIMESTAMP_PREFIX = "res_timestamp-";
40     private static final String[] SUPPORTED_ABIS = getSupportedAbis();
41 
42     @SuppressWarnings("deprecation")
getVersionCode(@onNull PackageInfo packageInfo)43     static long getVersionCode(@NonNull PackageInfo packageInfo) {
44         // Linter needs P (28) hardcoded or else it will fail these lines.
45         if (Build.VERSION.SDK_INT >= 28) {
46             return packageInfo.getLongVersionCode();
47         } else {
48             return packageInfo.versionCode;
49         }
50     }
51 
52     private static class ExtractTask extends AsyncTask<Void, Void, Void> {
53         @NonNull
54         private final String mDataDirPath;
55         @NonNull
56         private final HashSet<String> mResources;
57         @NonNull
58         private final AssetManager mAssetManager;
59         @NonNull
60         private final String mPackageName;
61         @NonNull
62         private final PackageManager mPackageManager;
63 
ExtractTask(@onNull String dataDirPath, @NonNull HashSet<String> resources, @NonNull String packageName, @NonNull PackageManager packageManager, @NonNull AssetManager assetManager)64         ExtractTask(@NonNull String dataDirPath,
65                     @NonNull HashSet<String> resources,
66                     @NonNull String packageName,
67                     @NonNull PackageManager packageManager,
68                     @NonNull AssetManager assetManager) {
69             mDataDirPath = dataDirPath;
70             mResources = resources;
71             mAssetManager = assetManager;
72             mPackageName = packageName;
73             mPackageManager = packageManager;
74         }
75 
76         @Override
doInBackground(Void... unused)77         protected Void doInBackground(Void... unused) {
78             final File dataDir = new File(mDataDirPath);
79 
80             final String timestamp = checkTimestamp(dataDir, mPackageManager, mPackageName);
81             if (timestamp == null) {
82                 return null;
83             }
84 
85             deleteFiles(mDataDirPath, mResources);
86 
87             if (!extractAPK(dataDir)) {
88                 return null;
89             }
90 
91             if (timestamp != null) {
92                 try {
93                     new File(dataDir, timestamp).createNewFile();
94                 } catch (IOException e) {
95                     Log.w(TAG, "Failed to write resource timestamp");
96                 }
97             }
98 
99             return null;
100         }
101 
102 
103         /// Returns true if successfully unpacked APK resources,
104         /// otherwise deletes all resources and returns false.
105         @WorkerThread
extractAPK(@onNull File dataDir)106         private boolean extractAPK(@NonNull File dataDir) {
107             for (String asset : mResources) {
108                 try {
109                     final String resource = "assets/" + asset;
110                     final File output = new File(dataDir, asset);
111                     if (output.exists()) {
112                         continue;
113                     }
114                     if (output.getParentFile() != null) {
115                         output.getParentFile().mkdirs();
116                     }
117 
118                     try (InputStream is = mAssetManager.open(asset);
119                         OutputStream os = new FileOutputStream(output)) {
120                         copy(is, os);
121                     }
122                     if (BuildConfig.DEBUG) {
123                         Log.i(TAG, "Extracted baseline resource " + resource);
124                     }
125                 } catch (FileNotFoundException fnfe) {
126                     continue;
127 
128                 } catch (IOException ioe) {
129                     Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
130                     deleteFiles(mDataDirPath, mResources);
131                     return false;
132                 }
133             }
134 
135             return true;
136         }
137     }
138 
139     @NonNull
140     private final String mDataDirPath;
141     @NonNull
142     private final String mPackageName;
143     @NonNull
144     private final PackageManager mPackageManager;
145     @NonNull
146     private final AssetManager mAssetManager;
147     @NonNull
148     private final HashSet<String> mResources;
149     private ExtractTask mExtractTask;
150 
ResourceExtractor(@onNull String dataDirPath, @NonNull String packageName, @NonNull PackageManager packageManager, @NonNull AssetManager assetManager)151     ResourceExtractor(@NonNull String dataDirPath,
152                       @NonNull String packageName,
153                       @NonNull PackageManager packageManager,
154                       @NonNull AssetManager assetManager) {
155         mDataDirPath = dataDirPath;
156         mPackageName = packageName;
157         mPackageManager = packageManager;
158         mAssetManager = assetManager;
159         mResources = new HashSet<>();
160     }
161 
addResource(@onNull String resource)162     ResourceExtractor addResource(@NonNull String resource) {
163         mResources.add(resource);
164         return this;
165     }
166 
addResources(@onNull Collection<String> resources)167     ResourceExtractor addResources(@NonNull Collection<String> resources) {
168         mResources.addAll(resources);
169         return this;
170     }
171 
start()172     ResourceExtractor start() {
173         if (BuildConfig.DEBUG && mExtractTask != null) {
174             Log.e(TAG, "Attempted to start resource extraction while another extraction was in progress.");
175         }
176         mExtractTask = new ExtractTask(mDataDirPath, mResources, mPackageName, mPackageManager, mAssetManager);
177         mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
178         return this;
179     }
180 
waitForCompletion()181     void waitForCompletion() {
182         if (mExtractTask == null) {
183             return;
184         }
185 
186         try {
187             mExtractTask.get();
188         } catch (CancellationException | ExecutionException | InterruptedException e) {
189             deleteFiles(mDataDirPath, mResources);
190         }
191     }
192 
getExistingTimestamps(File dataDir)193     private static String[] getExistingTimestamps(File dataDir) {
194         return dataDir.list(new FilenameFilter() {
195             @Override
196             public boolean accept(File dir, String name) {
197                 return name.startsWith(TIMESTAMP_PREFIX);
198             }
199         });
200     }
201 
202     private static void deleteFiles(@NonNull String dataDirPath, @NonNull HashSet<String> resources) {
203         final File dataDir = new File(dataDirPath);
204         for (String resource : resources) {
205             final File file = new File(dataDir, resource);
206             if (file.exists()) {
207                 file.delete();
208             }
209         }
210         final String[] existingTimestamps = getExistingTimestamps(dataDir);
211         if (existingTimestamps == null) {
212             return;
213         }
214         for (String timestamp : existingTimestamps) {
215             new File(dataDir, timestamp).delete();
216         }
217     }
218 
219     // Returns null if extracted resources are found and match the current APK version
220     // and update version if any, otherwise returns the current APK and update version.
221     private static String checkTimestamp(@NonNull File dataDir,
222                                          @NonNull PackageManager packageManager,
223                                          @NonNull String packageName) {
224         PackageInfo packageInfo = null;
225 
226         try {
227             packageInfo = packageManager.getPackageInfo(packageName, 0);
228         } catch (PackageManager.NameNotFoundException e) {
229             return TIMESTAMP_PREFIX;
230         }
231 
232         if (packageInfo == null) {
233             return TIMESTAMP_PREFIX;
234         }
235 
236         String expectedTimestamp =
237                 TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;
238 
239         final String[] existingTimestamps = getExistingTimestamps(dataDir);
240 
241         if (existingTimestamps == null) {
242             if (BuildConfig.DEBUG) {
243                 Log.i(TAG, "No extracted resources found");
244             }
245             return expectedTimestamp;
246         }
247 
248         if (existingTimestamps.length == 1) {
249             if (BuildConfig.DEBUG) {
250                 Log.i(TAG, "Found extracted resources " + existingTimestamps[0]);
251             }
252         }
253 
254         if (existingTimestamps.length != 1
255                 || !expectedTimestamp.equals(existingTimestamps[0])) {
256             if (BuildConfig.DEBUG) {
257                 Log.i(TAG, "Resource version mismatch " + expectedTimestamp);
258             }
259             return expectedTimestamp;
260         }
261 
262         return null;
263     }
264 
265     private static void copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
266         byte[] buf = new byte[16 * 1024];
267         for (int i; (i = in.read(buf)) >= 0; ) {
268             out.write(buf, 0, i);
269         }
270     }
271 
272     @SuppressWarnings("deprecation")
273     private static String[] getSupportedAbis() {
274         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
275             return Build.SUPPORTED_ABIS;
276         } else {
277             ArrayList<String> cpuAbis = new ArrayList<String>(asList(Build.CPU_ABI, Build.CPU_ABI2));
278             cpuAbis.removeAll(asList(null, ""));
279             return cpuAbis.toArray(new String[0]);
280         }
281     }
282 }
283