michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko; michael@0: michael@0: import org.mozilla.gecko.mozglue.RobocopTarget; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: michael@0: import android.app.Activity; michael@0: import android.content.Context; michael@0: import android.content.SharedPreferences; michael@0: import android.util.Log; michael@0: michael@0: import java.io.File; michael@0: import java.io.FileOutputStream; michael@0: import java.io.IOException; michael@0: import java.io.InputStream; michael@0: import java.io.OutputStream; michael@0: import java.util.Collections; michael@0: import java.util.Enumeration; michael@0: import java.util.HashMap; michael@0: import java.util.Iterator; michael@0: import java.util.Map; michael@0: import java.util.Scanner; michael@0: import java.util.zip.ZipEntry; michael@0: import java.util.zip.ZipFile; michael@0: michael@0: public final class Distribution { michael@0: private static final String LOGTAG = "GeckoDistribution"; michael@0: michael@0: private static final int STATE_UNKNOWN = 0; michael@0: private static final int STATE_NONE = 1; michael@0: private static final int STATE_SET = 2; michael@0: michael@0: public static class DistributionDescriptor { michael@0: public final boolean valid; michael@0: public final String id; michael@0: public final String version; // Example uses a float, but that's a crazy idea. michael@0: michael@0: // Default UI-visible description of the distribution. michael@0: public final String about; michael@0: michael@0: // Each distribution file can include multiple localized versions of michael@0: // the 'about' string. These are represented as, e.g., "about.en-US" michael@0: // keys in the Global object. michael@0: // Here we map locale to description. michael@0: public final Map localizedAbout; michael@0: michael@0: @SuppressWarnings("unchecked") michael@0: public DistributionDescriptor(JSONObject obj) { michael@0: this.id = obj.optString("id"); michael@0: this.version = obj.optString("version"); michael@0: this.about = obj.optString("about"); michael@0: Map loc = new HashMap(); michael@0: try { michael@0: Iterator keys = obj.keys(); michael@0: while (keys.hasNext()) { michael@0: String key = keys.next(); michael@0: if (key.startsWith("about.")) { michael@0: String locale = key.substring(6); michael@0: if (!obj.isNull(locale)) { michael@0: loc.put(locale, obj.getString(key)); michael@0: } michael@0: } michael@0: } michael@0: } catch (JSONException ex) { michael@0: Log.w(LOGTAG, "Unable to completely process distribution JSON.", ex); michael@0: } michael@0: michael@0: this.localizedAbout = Collections.unmodifiableMap(loc); michael@0: this.valid = (null != this.id) && michael@0: (null != this.version) && michael@0: (null != this.about); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Initializes distribution if it hasn't already been initalized. Sends michael@0: * messages to Gecko as appropriate. michael@0: * michael@0: * @param packagePath where to look for the distribution directory. michael@0: */ michael@0: @RobocopTarget michael@0: public static void init(final Context context, final String packagePath, final String prefsPath) { michael@0: // Read/write preferences and files on the background thread. michael@0: ThreadUtils.postToBackgroundThread(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: Distribution dist = new Distribution(context, packagePath, prefsPath); michael@0: boolean distributionSet = dist.doInit(); michael@0: if (distributionSet) { michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", "")); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Use Context.getPackageResourcePath to find an implicit michael@0: * package path. michael@0: */ michael@0: public static void init(final Context context) { michael@0: Distribution.init(context, context.getPackageResourcePath(), null); michael@0: } michael@0: michael@0: /** michael@0: * Returns parsed contents of bookmarks.json. michael@0: * This method should only be called from a background thread. michael@0: */ michael@0: public static JSONArray getBookmarks(final Context context) { michael@0: Distribution dist = new Distribution(context); michael@0: return dist.getBookmarks(); michael@0: } michael@0: michael@0: private final Context context; michael@0: private final String packagePath; michael@0: private final String prefsBranch; michael@0: michael@0: private int state = STATE_UNKNOWN; michael@0: private File distributionDir = null; michael@0: michael@0: /** michael@0: * @param packagePath where to look for the distribution directory. michael@0: */ michael@0: public Distribution(final Context context, final String packagePath, final String prefsBranch) { michael@0: this.context = context; michael@0: this.packagePath = packagePath; michael@0: this.prefsBranch = prefsBranch; michael@0: } michael@0: michael@0: public Distribution(final Context context) { michael@0: this(context, context.getPackageResourcePath(), null); michael@0: } michael@0: michael@0: /** michael@0: * Don't call from the main thread. michael@0: * michael@0: * @return true if we've set a distribution. michael@0: */ michael@0: private boolean doInit() { michael@0: // Bail if we've already tried to initialize the distribution, and michael@0: // there wasn't one. michael@0: final SharedPreferences settings; michael@0: if (prefsBranch == null) { michael@0: settings = GeckoSharedPrefs.forApp(context); michael@0: } else { michael@0: settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE); michael@0: } michael@0: michael@0: String keyName = context.getPackageName() + ".distribution_state"; michael@0: this.state = settings.getInt(keyName, STATE_UNKNOWN); michael@0: if (this.state == STATE_NONE) { michael@0: return false; michael@0: } michael@0: michael@0: // We've done the work once; don't do it again. michael@0: if (this.state == STATE_SET) { michael@0: // Note that we don't compute the distribution directory. michael@0: // Call `ensureDistributionDir` if you need it. michael@0: return true; michael@0: } michael@0: michael@0: boolean distributionSet = false; michael@0: try { michael@0: // First, try copying distribution files out of the APK. michael@0: distributionSet = copyFiles(); michael@0: if (distributionSet) { michael@0: // We always copy to the data dir, and we only copy files from michael@0: // a 'distribution' subdirectory. Track our dist dir now that michael@0: // we know it. michael@0: this.distributionDir = new File(getDataDir(), "distribution/"); michael@0: } michael@0: } catch (IOException e) { michael@0: Log.e(LOGTAG, "Error copying distribution files", e); michael@0: } michael@0: michael@0: if (!distributionSet) { michael@0: // If there aren't any distribution files in the APK, look in the /system directory. michael@0: File distDir = getSystemDistributionDir(); michael@0: if (distDir.exists()) { michael@0: distributionSet = true; michael@0: this.distributionDir = distDir; michael@0: } michael@0: } michael@0: michael@0: this.state = distributionSet ? STATE_SET : STATE_NONE; michael@0: settings.edit().putInt(keyName, this.state).commit(); michael@0: return distributionSet; michael@0: } michael@0: michael@0: /** michael@0: * Copies the /distribution folder out of the APK and into the app's data directory. michael@0: * Returns true if distribution files were found and copied. michael@0: */ michael@0: private boolean copyFiles() throws IOException { michael@0: File applicationPackage = new File(packagePath); michael@0: ZipFile zip = new ZipFile(applicationPackage); michael@0: michael@0: boolean distributionSet = false; michael@0: Enumeration zipEntries = zip.entries(); michael@0: michael@0: byte[] buffer = new byte[1024]; michael@0: while (zipEntries.hasMoreElements()) { michael@0: ZipEntry fileEntry = zipEntries.nextElement(); michael@0: String name = fileEntry.getName(); michael@0: michael@0: if (!name.startsWith("distribution/")) { michael@0: continue; michael@0: } michael@0: michael@0: distributionSet = true; michael@0: michael@0: File outFile = new File(getDataDir(), name); michael@0: File dir = outFile.getParentFile(); michael@0: michael@0: if (!dir.exists()) { michael@0: if (!dir.mkdirs()) { michael@0: Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath()); michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: InputStream fileStream = zip.getInputStream(fileEntry); michael@0: OutputStream outStream = new FileOutputStream(outFile); michael@0: michael@0: int count; michael@0: while ((count = fileStream.read(buffer)) != -1) { michael@0: outStream.write(buffer, 0, count); michael@0: } michael@0: michael@0: fileStream.close(); michael@0: outStream.close(); michael@0: outFile.setLastModified(fileEntry.getTime()); michael@0: } michael@0: michael@0: zip.close(); michael@0: michael@0: return distributionSet; michael@0: } michael@0: michael@0: /** michael@0: * After calling this method, either distributionDir michael@0: * will be set, or there is no distribution in use. michael@0: * michael@0: * Only call after init. michael@0: */ michael@0: private File ensureDistributionDir() { michael@0: if (this.distributionDir != null) { michael@0: return this.distributionDir; michael@0: } michael@0: michael@0: if (this.state != STATE_SET) { michael@0: return null; michael@0: } michael@0: michael@0: // After init, we know that either we've copied a distribution out of michael@0: // the APK, or it exists in /system/. michael@0: // Look in each location in turn. michael@0: // (This could be optimized by caching the path in shared prefs.) michael@0: File copied = new File(getDataDir(), "distribution/"); michael@0: if (copied.exists()) { michael@0: return this.distributionDir = copied; michael@0: } michael@0: File system = getSystemDistributionDir(); michael@0: if (system.exists()) { michael@0: return this.distributionDir = system; michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: /** michael@0: * Helper to grab a file in the distribution directory. michael@0: * michael@0: * Returns null if there is no distribution directory or the file michael@0: * doesn't exist. Ensures init first. michael@0: */ michael@0: public File getDistributionFile(String name) { michael@0: Log.i(LOGTAG, "Getting file from distribution."); michael@0: if (this.state == STATE_UNKNOWN) { michael@0: if (!this.doInit()) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: File dist = ensureDistributionDir(); michael@0: if (dist == null) { michael@0: return null; michael@0: } michael@0: michael@0: File descFile = new File(dist, name); michael@0: if (!descFile.exists()) { michael@0: Log.e(LOGTAG, "Distribution directory exists, but no file named " + name); michael@0: return null; michael@0: } michael@0: michael@0: return descFile; michael@0: } michael@0: michael@0: public DistributionDescriptor getDescriptor() { michael@0: File descFile = getDistributionFile("preferences.json"); michael@0: if (descFile == null) { michael@0: // Logging and existence checks are handled in getDistributionFile. michael@0: return null; michael@0: } michael@0: michael@0: try { michael@0: JSONObject all = new JSONObject(getFileContents(descFile)); michael@0: michael@0: if (!all.has("Global")) { michael@0: Log.e(LOGTAG, "Distribution preferences.json has no Global entry!"); michael@0: return null; michael@0: } michael@0: michael@0: return new DistributionDescriptor(all.getJSONObject("Global")); michael@0: michael@0: } catch (IOException e) { michael@0: Log.e(LOGTAG, "Error getting distribution descriptor file.", e); michael@0: return null; michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "Error parsing preferences.json", e); michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: public JSONArray getBookmarks() { michael@0: File bookmarks = getDistributionFile("bookmarks.json"); michael@0: if (bookmarks == null) { michael@0: // Logging and existence checks are handled in getDistributionFile. michael@0: return null; michael@0: } michael@0: michael@0: try { michael@0: return new JSONArray(getFileContents(bookmarks)); michael@0: } catch (IOException e) { michael@0: Log.e(LOGTAG, "Error getting bookmarks", e); michael@0: } catch (JSONException e) { michael@0: Log.e(LOGTAG, "Error parsing bookmarks.json", e); michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: michael@0: // Shortcut to slurp a file without messing around with streams. michael@0: private String getFileContents(File file) throws IOException { michael@0: Scanner scanner = null; michael@0: try { michael@0: scanner = new Scanner(file, "UTF-8"); michael@0: return scanner.useDelimiter("\\A").next(); michael@0: } finally { michael@0: if (scanner != null) { michael@0: scanner.close(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: private String getDataDir() { michael@0: return context.getApplicationInfo().dataDir; michael@0: } michael@0: michael@0: private File getSystemDistributionDir() { michael@0: return new File("/system/" + context.getPackageName() + "/distribution"); michael@0: } michael@0: }