mobile/android/base/Distribution.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

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

mercurial