1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/Distribution.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,369 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.mozglue.RobocopTarget; 1.12 +import org.mozilla.gecko.util.ThreadUtils; 1.13 + 1.14 +import org.json.JSONArray; 1.15 +import org.json.JSONException; 1.16 +import org.json.JSONObject; 1.17 + 1.18 +import android.app.Activity; 1.19 +import android.content.Context; 1.20 +import android.content.SharedPreferences; 1.21 +import android.util.Log; 1.22 + 1.23 +import java.io.File; 1.24 +import java.io.FileOutputStream; 1.25 +import java.io.IOException; 1.26 +import java.io.InputStream; 1.27 +import java.io.OutputStream; 1.28 +import java.util.Collections; 1.29 +import java.util.Enumeration; 1.30 +import java.util.HashMap; 1.31 +import java.util.Iterator; 1.32 +import java.util.Map; 1.33 +import java.util.Scanner; 1.34 +import java.util.zip.ZipEntry; 1.35 +import java.util.zip.ZipFile; 1.36 + 1.37 +public final class Distribution { 1.38 + private static final String LOGTAG = "GeckoDistribution"; 1.39 + 1.40 + private static final int STATE_UNKNOWN = 0; 1.41 + private static final int STATE_NONE = 1; 1.42 + private static final int STATE_SET = 2; 1.43 + 1.44 + public static class DistributionDescriptor { 1.45 + public final boolean valid; 1.46 + public final String id; 1.47 + public final String version; // Example uses a float, but that's a crazy idea. 1.48 + 1.49 + // Default UI-visible description of the distribution. 1.50 + public final String about; 1.51 + 1.52 + // Each distribution file can include multiple localized versions of 1.53 + // the 'about' string. These are represented as, e.g., "about.en-US" 1.54 + // keys in the Global object. 1.55 + // Here we map locale to description. 1.56 + public final Map<String, String> localizedAbout; 1.57 + 1.58 + @SuppressWarnings("unchecked") 1.59 + public DistributionDescriptor(JSONObject obj) { 1.60 + this.id = obj.optString("id"); 1.61 + this.version = obj.optString("version"); 1.62 + this.about = obj.optString("about"); 1.63 + Map<String, String> loc = new HashMap<String, String>(); 1.64 + try { 1.65 + Iterator<String> keys = obj.keys(); 1.66 + while (keys.hasNext()) { 1.67 + String key = keys.next(); 1.68 + if (key.startsWith("about.")) { 1.69 + String locale = key.substring(6); 1.70 + if (!obj.isNull(locale)) { 1.71 + loc.put(locale, obj.getString(key)); 1.72 + } 1.73 + } 1.74 + } 1.75 + } catch (JSONException ex) { 1.76 + Log.w(LOGTAG, "Unable to completely process distribution JSON.", ex); 1.77 + } 1.78 + 1.79 + this.localizedAbout = Collections.unmodifiableMap(loc); 1.80 + this.valid = (null != this.id) && 1.81 + (null != this.version) && 1.82 + (null != this.about); 1.83 + } 1.84 + } 1.85 + 1.86 + /** 1.87 + * Initializes distribution if it hasn't already been initalized. Sends 1.88 + * messages to Gecko as appropriate. 1.89 + * 1.90 + * @param packagePath where to look for the distribution directory. 1.91 + */ 1.92 + @RobocopTarget 1.93 + public static void init(final Context context, final String packagePath, final String prefsPath) { 1.94 + // Read/write preferences and files on the background thread. 1.95 + ThreadUtils.postToBackgroundThread(new Runnable() { 1.96 + @Override 1.97 + public void run() { 1.98 + Distribution dist = new Distribution(context, packagePath, prefsPath); 1.99 + boolean distributionSet = dist.doInit(); 1.100 + if (distributionSet) { 1.101 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", "")); 1.102 + } 1.103 + } 1.104 + }); 1.105 + } 1.106 + 1.107 + /** 1.108 + * Use <code>Context.getPackageResourcePath</code> to find an implicit 1.109 + * package path. 1.110 + */ 1.111 + public static void init(final Context context) { 1.112 + Distribution.init(context, context.getPackageResourcePath(), null); 1.113 + } 1.114 + 1.115 + /** 1.116 + * Returns parsed contents of bookmarks.json. 1.117 + * This method should only be called from a background thread. 1.118 + */ 1.119 + public static JSONArray getBookmarks(final Context context) { 1.120 + Distribution dist = new Distribution(context); 1.121 + return dist.getBookmarks(); 1.122 + } 1.123 + 1.124 + private final Context context; 1.125 + private final String packagePath; 1.126 + private final String prefsBranch; 1.127 + 1.128 + private int state = STATE_UNKNOWN; 1.129 + private File distributionDir = null; 1.130 + 1.131 + /** 1.132 + * @param packagePath where to look for the distribution directory. 1.133 + */ 1.134 + public Distribution(final Context context, final String packagePath, final String prefsBranch) { 1.135 + this.context = context; 1.136 + this.packagePath = packagePath; 1.137 + this.prefsBranch = prefsBranch; 1.138 + } 1.139 + 1.140 + public Distribution(final Context context) { 1.141 + this(context, context.getPackageResourcePath(), null); 1.142 + } 1.143 + 1.144 + /** 1.145 + * Don't call from the main thread. 1.146 + * 1.147 + * @return true if we've set a distribution. 1.148 + */ 1.149 + private boolean doInit() { 1.150 + // Bail if we've already tried to initialize the distribution, and 1.151 + // there wasn't one. 1.152 + final SharedPreferences settings; 1.153 + if (prefsBranch == null) { 1.154 + settings = GeckoSharedPrefs.forApp(context); 1.155 + } else { 1.156 + settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE); 1.157 + } 1.158 + 1.159 + String keyName = context.getPackageName() + ".distribution_state"; 1.160 + this.state = settings.getInt(keyName, STATE_UNKNOWN); 1.161 + if (this.state == STATE_NONE) { 1.162 + return false; 1.163 + } 1.164 + 1.165 + // We've done the work once; don't do it again. 1.166 + if (this.state == STATE_SET) { 1.167 + // Note that we don't compute the distribution directory. 1.168 + // Call `ensureDistributionDir` if you need it. 1.169 + return true; 1.170 + } 1.171 + 1.172 + boolean distributionSet = false; 1.173 + try { 1.174 + // First, try copying distribution files out of the APK. 1.175 + distributionSet = copyFiles(); 1.176 + if (distributionSet) { 1.177 + // We always copy to the data dir, and we only copy files from 1.178 + // a 'distribution' subdirectory. Track our dist dir now that 1.179 + // we know it. 1.180 + this.distributionDir = new File(getDataDir(), "distribution/"); 1.181 + } 1.182 + } catch (IOException e) { 1.183 + Log.e(LOGTAG, "Error copying distribution files", e); 1.184 + } 1.185 + 1.186 + if (!distributionSet) { 1.187 + // If there aren't any distribution files in the APK, look in the /system directory. 1.188 + File distDir = getSystemDistributionDir(); 1.189 + if (distDir.exists()) { 1.190 + distributionSet = true; 1.191 + this.distributionDir = distDir; 1.192 + } 1.193 + } 1.194 + 1.195 + this.state = distributionSet ? STATE_SET : STATE_NONE; 1.196 + settings.edit().putInt(keyName, this.state).commit(); 1.197 + return distributionSet; 1.198 + } 1.199 + 1.200 + /** 1.201 + * Copies the /distribution folder out of the APK and into the app's data directory. 1.202 + * Returns true if distribution files were found and copied. 1.203 + */ 1.204 + private boolean copyFiles() throws IOException { 1.205 + File applicationPackage = new File(packagePath); 1.206 + ZipFile zip = new ZipFile(applicationPackage); 1.207 + 1.208 + boolean distributionSet = false; 1.209 + Enumeration<? extends ZipEntry> zipEntries = zip.entries(); 1.210 + 1.211 + byte[] buffer = new byte[1024]; 1.212 + while (zipEntries.hasMoreElements()) { 1.213 + ZipEntry fileEntry = zipEntries.nextElement(); 1.214 + String name = fileEntry.getName(); 1.215 + 1.216 + if (!name.startsWith("distribution/")) { 1.217 + continue; 1.218 + } 1.219 + 1.220 + distributionSet = true; 1.221 + 1.222 + File outFile = new File(getDataDir(), name); 1.223 + File dir = outFile.getParentFile(); 1.224 + 1.225 + if (!dir.exists()) { 1.226 + if (!dir.mkdirs()) { 1.227 + Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath()); 1.228 + continue; 1.229 + } 1.230 + } 1.231 + 1.232 + InputStream fileStream = zip.getInputStream(fileEntry); 1.233 + OutputStream outStream = new FileOutputStream(outFile); 1.234 + 1.235 + int count; 1.236 + while ((count = fileStream.read(buffer)) != -1) { 1.237 + outStream.write(buffer, 0, count); 1.238 + } 1.239 + 1.240 + fileStream.close(); 1.241 + outStream.close(); 1.242 + outFile.setLastModified(fileEntry.getTime()); 1.243 + } 1.244 + 1.245 + zip.close(); 1.246 + 1.247 + return distributionSet; 1.248 + } 1.249 + 1.250 + /** 1.251 + * After calling this method, either <code>distributionDir</code> 1.252 + * will be set, or there is no distribution in use. 1.253 + * 1.254 + * Only call after init. 1.255 + */ 1.256 + private File ensureDistributionDir() { 1.257 + if (this.distributionDir != null) { 1.258 + return this.distributionDir; 1.259 + } 1.260 + 1.261 + if (this.state != STATE_SET) { 1.262 + return null; 1.263 + } 1.264 + 1.265 + // After init, we know that either we've copied a distribution out of 1.266 + // the APK, or it exists in /system/. 1.267 + // Look in each location in turn. 1.268 + // (This could be optimized by caching the path in shared prefs.) 1.269 + File copied = new File(getDataDir(), "distribution/"); 1.270 + if (copied.exists()) { 1.271 + return this.distributionDir = copied; 1.272 + } 1.273 + File system = getSystemDistributionDir(); 1.274 + if (system.exists()) { 1.275 + return this.distributionDir = system; 1.276 + } 1.277 + return null; 1.278 + } 1.279 + 1.280 + /** 1.281 + * Helper to grab a file in the distribution directory. 1.282 + * 1.283 + * Returns null if there is no distribution directory or the file 1.284 + * doesn't exist. Ensures init first. 1.285 + */ 1.286 + public File getDistributionFile(String name) { 1.287 + Log.i(LOGTAG, "Getting file from distribution."); 1.288 + if (this.state == STATE_UNKNOWN) { 1.289 + if (!this.doInit()) { 1.290 + return null; 1.291 + } 1.292 + } 1.293 + 1.294 + File dist = ensureDistributionDir(); 1.295 + if (dist == null) { 1.296 + return null; 1.297 + } 1.298 + 1.299 + File descFile = new File(dist, name); 1.300 + if (!descFile.exists()) { 1.301 + Log.e(LOGTAG, "Distribution directory exists, but no file named " + name); 1.302 + return null; 1.303 + } 1.304 + 1.305 + return descFile; 1.306 + } 1.307 + 1.308 + public DistributionDescriptor getDescriptor() { 1.309 + File descFile = getDistributionFile("preferences.json"); 1.310 + if (descFile == null) { 1.311 + // Logging and existence checks are handled in getDistributionFile. 1.312 + return null; 1.313 + } 1.314 + 1.315 + try { 1.316 + JSONObject all = new JSONObject(getFileContents(descFile)); 1.317 + 1.318 + if (!all.has("Global")) { 1.319 + Log.e(LOGTAG, "Distribution preferences.json has no Global entry!"); 1.320 + return null; 1.321 + } 1.322 + 1.323 + return new DistributionDescriptor(all.getJSONObject("Global")); 1.324 + 1.325 + } catch (IOException e) { 1.326 + Log.e(LOGTAG, "Error getting distribution descriptor file.", e); 1.327 + return null; 1.328 + } catch (JSONException e) { 1.329 + Log.e(LOGTAG, "Error parsing preferences.json", e); 1.330 + return null; 1.331 + } 1.332 + } 1.333 + 1.334 + public JSONArray getBookmarks() { 1.335 + File bookmarks = getDistributionFile("bookmarks.json"); 1.336 + if (bookmarks == null) { 1.337 + // Logging and existence checks are handled in getDistributionFile. 1.338 + return null; 1.339 + } 1.340 + 1.341 + try { 1.342 + return new JSONArray(getFileContents(bookmarks)); 1.343 + } catch (IOException e) { 1.344 + Log.e(LOGTAG, "Error getting bookmarks", e); 1.345 + } catch (JSONException e) { 1.346 + Log.e(LOGTAG, "Error parsing bookmarks.json", e); 1.347 + } 1.348 + 1.349 + return null; 1.350 + } 1.351 + 1.352 + // Shortcut to slurp a file without messing around with streams. 1.353 + private String getFileContents(File file) throws IOException { 1.354 + Scanner scanner = null; 1.355 + try { 1.356 + scanner = new Scanner(file, "UTF-8"); 1.357 + return scanner.useDelimiter("\\A").next(); 1.358 + } finally { 1.359 + if (scanner != null) { 1.360 + scanner.close(); 1.361 + } 1.362 + } 1.363 + } 1.364 + 1.365 + private String getDataDir() { 1.366 + return context.getApplicationInfo().dataDir; 1.367 + } 1.368 + 1.369 + private File getSystemDistributionDir() { 1.370 + return new File("/system/" + context.getPackageName() + "/distribution"); 1.371 + } 1.372 +}