mobile/android/base/Distribution.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial