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