mobile/android/base/Distribution.java

branch
TOR_BUG_9701
changeset 15
b8a032363ba2
equal deleted inserted replaced
-1:000000000000 0:f273ea1abe7a
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 package org.mozilla.gecko;
7
8 import org.mozilla.gecko.mozglue.RobocopTarget;
9 import org.mozilla.gecko.util.ThreadUtils;
10
11 import org.json.JSONArray;
12 import org.json.JSONException;
13 import org.json.JSONObject;
14
15 import android.app.Activity;
16 import android.content.Context;
17 import android.content.SharedPreferences;
18 import android.util.Log;
19
20 import java.io.File;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.util.Collections;
26 import java.util.Enumeration;
27 import java.util.HashMap;
28 import java.util.Iterator;
29 import java.util.Map;
30 import java.util.Scanner;
31 import java.util.zip.ZipEntry;
32 import java.util.zip.ZipFile;
33
34 public final class Distribution {
35 private static final String LOGTAG = "GeckoDistribution";
36
37 private static final int STATE_UNKNOWN = 0;
38 private static final int STATE_NONE = 1;
39 private static final int STATE_SET = 2;
40
41 public static class DistributionDescriptor {
42 public final boolean valid;
43 public final String id;
44 public final String version; // Example uses a float, but that's a crazy idea.
45
46 // Default UI-visible description of the distribution.
47 public final String about;
48
49 // Each distribution file can include multiple localized versions of
50 // the 'about' string. These are represented as, e.g., "about.en-US"
51 // keys in the Global object.
52 // Here we map locale to description.
53 public final Map<String, String> localizedAbout;
54
55 @SuppressWarnings("unchecked")
56 public DistributionDescriptor(JSONObject obj) {
57 this.id = obj.optString("id");
58 this.version = obj.optString("version");
59 this.about = obj.optString("about");
60 Map<String, String> loc = new HashMap<String, String>();
61 try {
62 Iterator<String> keys = obj.keys();
63 while (keys.hasNext()) {
64 String key = keys.next();
65 if (key.startsWith("about.")) {
66 String locale = key.substring(6);
67 if (!obj.isNull(locale)) {
68 loc.put(locale, obj.getString(key));
69 }
70 }
71 }
72 } catch (JSONException ex) {
73 Log.w(LOGTAG, "Unable to completely process distribution JSON.", ex);
74 }
75
76 this.localizedAbout = Collections.unmodifiableMap(loc);
77 this.valid = (null != this.id) &&
78 (null != this.version) &&
79 (null != this.about);
80 }
81 }
82
83 /**
84 * Initializes distribution if it hasn't already been initalized. Sends
85 * messages to Gecko as appropriate.
86 *
87 * @param packagePath where to look for the distribution directory.
88 */
89 @RobocopTarget
90 public static void init(final Context context, final String packagePath, final String prefsPath) {
91 // Read/write preferences and files on the background thread.
92 ThreadUtils.postToBackgroundThread(new Runnable() {
93 @Override
94 public void run() {
95 Distribution dist = new Distribution(context, packagePath, prefsPath);
96 boolean distributionSet = dist.doInit();
97 if (distributionSet) {
98 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", ""));
99 }
100 }
101 });
102 }
103
104 /**
105 * Use <code>Context.getPackageResourcePath</code> to find an implicit
106 * package path.
107 */
108 public static void init(final Context context) {
109 Distribution.init(context, context.getPackageResourcePath(), null);
110 }
111
112 /**
113 * Returns parsed contents of bookmarks.json.
114 * This method should only be called from a background thread.
115 */
116 public static JSONArray getBookmarks(final Context context) {
117 Distribution dist = new Distribution(context);
118 return dist.getBookmarks();
119 }
120
121 private final Context context;
122 private final String packagePath;
123 private final String prefsBranch;
124
125 private int state = STATE_UNKNOWN;
126 private File distributionDir = null;
127
128 /**
129 * @param packagePath where to look for the distribution directory.
130 */
131 public Distribution(final Context context, final String packagePath, final String prefsBranch) {
132 this.context = context;
133 this.packagePath = packagePath;
134 this.prefsBranch = prefsBranch;
135 }
136
137 public Distribution(final Context context) {
138 this(context, context.getPackageResourcePath(), null);
139 }
140
141 /**
142 * Don't call from the main thread.
143 *
144 * @return true if we've set a distribution.
145 */
146 private boolean doInit() {
147 // Bail if we've already tried to initialize the distribution, and
148 // there wasn't one.
149 final SharedPreferences settings;
150 if (prefsBranch == null) {
151 settings = GeckoSharedPrefs.forApp(context);
152 } else {
153 settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE);
154 }
155
156 String keyName = context.getPackageName() + ".distribution_state";
157 this.state = settings.getInt(keyName, STATE_UNKNOWN);
158 if (this.state == STATE_NONE) {
159 return false;
160 }
161
162 // We've done the work once; don't do it again.
163 if (this.state == STATE_SET) {
164 // Note that we don't compute the distribution directory.
165 // Call `ensureDistributionDir` if you need it.
166 return true;
167 }
168
169 boolean distributionSet = false;
170 try {
171 // First, try copying distribution files out of the APK.
172 distributionSet = copyFiles();
173 if (distributionSet) {
174 // We always copy to the data dir, and we only copy files from
175 // a 'distribution' subdirectory. Track our dist dir now that
176 // we know it.
177 this.distributionDir = new File(getDataDir(), "distribution/");
178 }
179 } catch (IOException e) {
180 Log.e(LOGTAG, "Error copying distribution files", e);
181 }
182
183 if (!distributionSet) {
184 // If there aren't any distribution files in the APK, look in the /system directory.
185 File distDir = getSystemDistributionDir();
186 if (distDir.exists()) {
187 distributionSet = true;
188 this.distributionDir = distDir;
189 }
190 }
191
192 this.state = distributionSet ? STATE_SET : STATE_NONE;
193 settings.edit().putInt(keyName, this.state).commit();
194 return distributionSet;
195 }
196
197 /**
198 * Copies the /distribution folder out of the APK and into the app's data directory.
199 * Returns true if distribution files were found and copied.
200 */
201 private boolean copyFiles() throws IOException {
202 File applicationPackage = new File(packagePath);
203 ZipFile zip = new ZipFile(applicationPackage);
204
205 boolean distributionSet = false;
206 Enumeration<? extends ZipEntry> zipEntries = zip.entries();
207
208 byte[] buffer = new byte[1024];
209 while (zipEntries.hasMoreElements()) {
210 ZipEntry fileEntry = zipEntries.nextElement();
211 String name = fileEntry.getName();
212
213 if (!name.startsWith("distribution/")) {
214 continue;
215 }
216
217 distributionSet = true;
218
219 File outFile = new File(getDataDir(), name);
220 File dir = outFile.getParentFile();
221
222 if (!dir.exists()) {
223 if (!dir.mkdirs()) {
224 Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath());
225 continue;
226 }
227 }
228
229 InputStream fileStream = zip.getInputStream(fileEntry);
230 OutputStream outStream = new FileOutputStream(outFile);
231
232 int count;
233 while ((count = fileStream.read(buffer)) != -1) {
234 outStream.write(buffer, 0, count);
235 }
236
237 fileStream.close();
238 outStream.close();
239 outFile.setLastModified(fileEntry.getTime());
240 }
241
242 zip.close();
243
244 return distributionSet;
245 }
246
247 /**
248 * After calling this method, either <code>distributionDir</code>
249 * will be set, or there is no distribution in use.
250 *
251 * Only call after init.
252 */
253 private File ensureDistributionDir() {
254 if (this.distributionDir != null) {
255 return this.distributionDir;
256 }
257
258 if (this.state != STATE_SET) {
259 return null;
260 }
261
262 // After init, we know that either we've copied a distribution out of
263 // the APK, or it exists in /system/.
264 // Look in each location in turn.
265 // (This could be optimized by caching the path in shared prefs.)
266 File copied = new File(getDataDir(), "distribution/");
267 if (copied.exists()) {
268 return this.distributionDir = copied;
269 }
270 File system = getSystemDistributionDir();
271 if (system.exists()) {
272 return this.distributionDir = system;
273 }
274 return null;
275 }
276
277 /**
278 * Helper to grab a file in the distribution directory.
279 *
280 * Returns null if there is no distribution directory or the file
281 * doesn't exist. Ensures init first.
282 */
283 public File getDistributionFile(String name) {
284 Log.i(LOGTAG, "Getting file from distribution.");
285 if (this.state == STATE_UNKNOWN) {
286 if (!this.doInit()) {
287 return null;
288 }
289 }
290
291 File dist = ensureDistributionDir();
292 if (dist == null) {
293 return null;
294 }
295
296 File descFile = new File(dist, name);
297 if (!descFile.exists()) {
298 Log.e(LOGTAG, "Distribution directory exists, but no file named " + name);
299 return null;
300 }
301
302 return descFile;
303 }
304
305 public DistributionDescriptor getDescriptor() {
306 File descFile = getDistributionFile("preferences.json");
307 if (descFile == null) {
308 // Logging and existence checks are handled in getDistributionFile.
309 return null;
310 }
311
312 try {
313 JSONObject all = new JSONObject(getFileContents(descFile));
314
315 if (!all.has("Global")) {
316 Log.e(LOGTAG, "Distribution preferences.json has no Global entry!");
317 return null;
318 }
319
320 return new DistributionDescriptor(all.getJSONObject("Global"));
321
322 } catch (IOException e) {
323 Log.e(LOGTAG, "Error getting distribution descriptor file.", e);
324 return null;
325 } catch (JSONException e) {
326 Log.e(LOGTAG, "Error parsing preferences.json", e);
327 return null;
328 }
329 }
330
331 public JSONArray getBookmarks() {
332 File bookmarks = getDistributionFile("bookmarks.json");
333 if (bookmarks == null) {
334 // Logging and existence checks are handled in getDistributionFile.
335 return null;
336 }
337
338 try {
339 return new JSONArray(getFileContents(bookmarks));
340 } catch (IOException e) {
341 Log.e(LOGTAG, "Error getting bookmarks", e);
342 } catch (JSONException e) {
343 Log.e(LOGTAG, "Error parsing bookmarks.json", e);
344 }
345
346 return null;
347 }
348
349 // Shortcut to slurp a file without messing around with streams.
350 private String getFileContents(File file) throws IOException {
351 Scanner scanner = null;
352 try {
353 scanner = new Scanner(file, "UTF-8");
354 return scanner.useDelimiter("\\A").next();
355 } finally {
356 if (scanner != null) {
357 scanner.close();
358 }
359 }
360 }
361
362 private String getDataDir() {
363 return context.getApplicationInfo().dataDir;
364 }
365
366 private File getSystemDistributionDir() {
367 return new File("/system/" + context.getPackageName() + "/distribution");
368 }
369 }

mercurial