Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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 | } |