<|start_filename|>www/cdvah/views/about.html<|end_filename|>
<!--
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing,
  software distributed under the License is distributed on an
  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  KIND, either express or implied.  See the License for the
  specific language governing permissions and limitations
  under the License.
-->

<div class="title">{{appTitle}} <span class="title-version">v{{appVersion}}</span></div>
<div class="flex-1 scrollable" style="text-align:left">
<div>
<p>
    The Chrome App Developer Tool aids in the development of Chrome Apps for Mobile. It can be used to run Chrome Apps for Mobile with the Chrome Dev Editor, or with the cca tool, for faster development and easier debugging.
</p>
<p>
For more information, see the source and documentation <a href="http://goo.gl/2IfFPc">here</a>.
</p>
</div>
<div>
    <h2>Licences</h2>
    <p>{{appTitle}} is open source software, released under the <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License, Version 2.0</a></p>
    <p>This product contains other open source software. See the License files in <a href="https://github.com/MobileChromeApps/chrome-app-developer-tool">the source tree</a> for full details.</p>
    <p>This version of {{appTitle}} uses the Crosswalk project, which is released under a <a href="https://github.com/crosswalk-project/crosswalk/blob/master/LICENSE">BSD license</a>, and uses Chromium, also released under a <a href="http://src.chromium.org/viewvc/chrome/trunk/src/LICENSE">BSD license</a>.</p>
</div>

<div>
    <h2>Available Plugins</h2>
    <p>This version of Chrome App Developer Tool has support for the following plugins:</p>
    <ul>
        <li ng-repeat="plugin in plugins">
            <div><strong>{{ plugin.id }}</strong>: {{ plugin.version }}</div>
        </li>
    </ul>
</div>

<div class="hflex-container">
    <h2>Usage Statistics and Crash Reports</h2>
    <input type="checkbox" style="width:16px;height:16px" ng-model="formData.reportingPermissionCheckbox">
    <div class="flex-1" style="text-align:justify;margin:0 12px 0 14px">Help make {{appTitle}} better by automatically sending usage statistics and crash reports to Google. This information will be used in accordance with the <a href="http://www.google.com/policies/privacy/">Google Privacy Policy</a>.
    </div>
</div>
</div>
<div style="margin:20px">
    <button ng-click="goBack()" class="big-button">Done</button>
</div>


<|start_filename|>harness-push/harness-push.js<|end_filename|>
#!/usr/bin/env node
/**
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing,
  software distributed under the License is distributed on an
  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  KIND, either express or implied.  See the License for the
  specific language governing permissions and limitations
  under the License.
 */

var path = require('path');
var fs = require('fs');
try {
    var nopt = require('nopt');
} catch (e) {
    console.log('Please run: ( cd ' + __dirname + ' && npm install )');
    process.exit(1);
}
try {
    var HarnessClient = require('chrome-app-developer-tool-client');
} catch (e) {
    console.log('Please run: ( cd ' +
        path.join(__dirname, 'node_modules', 'chrome-app-developer-tool-client') +
        ' && npm install )');
    process.exit(1);
}

function parseArgs(argv) {
    var opts = {
        'help': Boolean,
        'target': String
    };
    var ret = nopt(opts, null, argv);
    return ret;
}

function usage() {
    console.log('Usage: harness-push push path/to/app --target=IP_ADDRESS:PORT');
    console.log('Usage: harness-push menu');
    console.log('Usage: harness-push eval "alert(1)"');
    console.log('Usage: harness-push info');
    console.log('Usage: harness-push launch [appId]');
    console.log('Usage: harness-push quit');
    console.log('Usage: harness-push assetmanifest [appId]');
    console.log('Usage: harness-push delete appId');
    console.log('Usage: harness-push deleteall');
    console.log('Usage: harness-push buildapk [appId] [--output=output.apk] [--keyProps=android-release-keys.properties]');
    console.log();
    console.log('--target defaults to localhost:2424');
    console.log('    To deploy to Android over USB: adb forward tcp:2424 tcp:2424');
    console.log('        "adb" comes with the Android SDK');
    console.log('    To deploy to iOS over USB: python tcprelay.py 2424:2424');
    console.log('        "tcprelay.py" is available from https://github.com/chid/tcprelay');
    process.exit(1);
}

function readPropertiesFile(propsFile) {
    var data = fs.readFileSync(propsFile, 'utf8');
    var pattern = /(.*?)=(.*)/g;
    var match;
    var ret = {};
    while ((match = pattern.exec(data))) {
        ret[match[1]] = match[2];
    }
    return ret;
}

function main() {
    var args = parseArgs(process.argv);

    function onFailure(err) {
        console.error(err);
    }
    function onSuccess(result) {
        if (typeof result.body == 'object') {
            console.log(JSON.stringify(result.body, null, 4));
        } else if (result.body) {
            console.log(result.body);
        }
    }

    var client = new HarnessClient(args.target);

    var cmd = args.argv.remain[0];
    if (cmd == 'push') {
        if (!args.argv.remain[1]) {
            usage();
        }
        var pushSession = client.createPushSession(args.argv.remain[1]);
        client.quit()
        .then(function() {
            return pushSession.push();
        }).then(onSuccess, onFailure);
    } else if (cmd == 'buildapk') {
        var propsFilePath = args.keyProps || 'android-release-keys.properties';
        var props = readPropertiesFile(propsFilePath);
        var certificatePath = props['certificateFile'];
        var privateKeyPath = props['privateKeyFile'];
        var keyStorePath = props['storeFile'];
        var appId = args.argv.remain[1];
        var outputPath = args.output || 'output.apk';
        var signingOpts = {
            keyPassword: props['keyPassword']
        };
        if (certificatePath && privateKeyPath) {
            signingOpts.certificatePath = path.join(path.dirname(propsFilePath), certificatePath);
            signingOpts.privateKeyPath = path.join(path.dirname(propsFilePath), privateKeyPath);
        } else if (keyStorePath) {
            signingOpts.keyStorePath = path.join(path.dirname(propsFilePath), keyStorePath);
            signingOpts.storeType = props['storeType'];
            signingOpts.storePassword = props['storePassword'];
            signingOpts.keyAlias = props['keyAlias'];
        } else {
            throw new Error('No key signing information within ' + propsFilePath);
        }

        client.buildApk(appId, 'chrome', signingOpts, outputPath).then(onSuccess, onFailure);
    } else if (cmd == 'deleteall') {
        client.deleteAllApps().then(onSuccess, onFailure);
    } else if (cmd == 'delete') {
        if (!args.argv.remain[1]) {
            usage();
        }
        client.deleteApp().then(onSuccess, onFailure);
    } else if (cmd == 'menu') {
        client.menu().then(onSuccess, onFailure);
    } else if (cmd == 'eval') {
        if (!args.argv.remain[1]) {
            usage();
        }
        client.evalJs(args.argv.remain[1]).then(onSuccess, onFailure);
    } else if (cmd == 'assetmanifest') {
        client.assetmanifest(args.appid).then(onSuccess, onFailure);
    } else if (cmd == 'info') {
        client.info().then(onSuccess, onFailure);
    } else if (cmd == 'launch') {
        client.launch(args.argv.remain[1]).then(onSuccess, onFailure);
    } else if (cmd == 'quit') {
        client.quit().then(onSuccess, onFailure);
    } else {
        usage();
    }
}

if (require.main === module) {
    main();
}


<|start_filename|>www/cdvah/js/ListCtrl.js<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
(function(){
    'use strict';
    /* global myApp */
    /* global chrome */
    myApp.controller('ListCtrl', ['$location', '$rootScope', '$scope', '$routeParams', '$q', 'AppsService', 'HarnessServer', 'AppHarnessUI', 'Reporter', 'APP_NAME', function ($location, $rootScope, $scope, $routeParams, $q, AppsService, HarnessServer, AppHarnessUI, Reporter, APP_NAME) {
        $scope.app = null;
        $scope.ipAddresses = null;
        $scope.port = null;

        function initialise() {
            $rootScope.appTitle = APP_NAME;
            $scope.$on('$destroy', function() {
                AppsService.onAppListChange = null;
            });
            $scope.port = 2424;
            AppsService.onAppListChange = loadAppsList;

            AppHarnessUI.setEventHandler(function(eventName) {
                if (eventName == 'showMenu') { // two-finger double tap
                    return AppHarnessUI.setVisible(false);
                } else if (eventName == 'hideMenu') {
                    return AppHarnessUI.setVisible(true);
                } else if (eventName == 'quitApp') {
                    return AppsService.quitApp();
                } else if (eventName == 'destroyed') {
                    return loadAppsList();
                } else {
                    console.warn('Unknown message from AppHarnessUI: ' + eventName);
                }
            });

            return loadAppsList()
            .then(function() {
                return HarnessServer.start();
            }).then(function() {
                var getInfoCallback = function() {
                    HarnessServer.getListenAddress(/* skipCache */ true)
                    .then(function(value) {
                        // No IP addresses happens when wifi is not connected.
                        $scope.ipAddresses = value ? value.split(', ') : ['127.0.0.1'];
                    });
                };

                // When getInfo is called, the callback is retained and called every time network info changes.
                // The callback updates the IP.
                navigator.connection.getInfo(getInfoCallback);
            }, function() {
                $scope.ipAddresses = [];
            }).then(function() {
                var deferred = $q.defer();

                // If this is the app's first run, show the first-launch screen.
                var hasRunDefault = { hasRun: 'empty' };
                var getHasRunCallback = function(data) {
                    var isFirstRun = (data.hasRun === 'empty');
                    if (isFirstRun) {
                        // This is the app's first run, so ask for permission!
                        $location.path('/permission');
                    }
                    // If this the first run, we don't want to send any events yet, since we want to ask permission first.
                    deferred.resolve(/* shouldSendEvents */ !isFirstRun);
                };

                // Check local storage for the first run key.
                chrome.storage.local.get(hasRunDefault, getHasRunCallback);

                return deferred.promise;
            }).then(function(shouldSendEvents) {
                if (shouldSendEvents) {
                    // Track the page view.
                    Reporter.sendPageView('list');

                    // Send a "CADT has launched" event, if we haven't already.
                    if (!$rootScope.appLaunchReported) {
                        Reporter.sendEvent('CADT', 'launched');
                        $rootScope.appLaunchReported = true;
                    }
                }
            });
        }

        function loadAppsList() {
            return AppsService.getAppList()
            .then(function(){
                $scope.app = AppsService.getLastAccessedApp();
                $scope.isRunning = !!AppsService.getActiveApp();
            }, function(error){
                console.error(error);
            });
        }

        $scope.launchApp = function(app){
            return AppsService.launchApp(app)
            .then(null, function(error){
                console.error(error);
            });
        };

        $scope.resumeApp = function(){
            return AppHarnessUI.setVisible(true);
        };

        $scope.stopApp = function(){
            return AppsService.quitApp();
        };

        $scope.removeApp = function() {
            // Uninstall all apps, since we only show the latest one, this most closely matches their intention.
            return AppsService.uninstallAllApps();
        };

        $scope.showDetails = function(index, ev) {
            ev.preventDefault();
            $location.path('/details/' + index);
        };

        return initialise();
    }]);
})();




<|start_filename|>www/cdvah/js/PluginMetadata.js<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
(function() {
    'use strict';
    /* global myApp */
    myApp.factory('PluginMetadata', function() {
        var harnessPluginList = cordova.require('cordova/plugin_list');
        var harnessPluginMetadata = harnessPluginList.metadata;

        // Returns -1, (a > b), 0 (a = b), or 1 (a < b).
        function semverCompare(a, b) {
            var regex = /^(\d+)\.(\d+)\.(\d+)/;
            var aComps = a.match(regex);
            var bComps = b.match(regex);

            for(var i = 1; i <= 3; i++) {
                if (+aComps[i] != +bComps[i]) {
                    return +aComps[i] < +bComps[i] ? 1 : -1;
                }
            }

            return 0;
        }

        return {
            availablePlugins: function() {
                var plugins = [];
                var pluginIds = Object.keys(harnessPluginMetadata);
                pluginIds.sort();
                for (var i=0; i < pluginIds.length; ++i) {
                    plugins.push({
                        id: pluginIds[i],
                        version: harnessPluginMetadata[pluginIds[i]]
                    });
                }
                return plugins;
            },
            extractPluginMetadata: function(pluginListFileContents) {
                if (!pluginListFileContents) {
                    throw new Error('cordova_plugins.js file is empty. Something has gone wrong with "cordova prepare".');
                }

                // Extract the JSON data from inside the JS file.
                // It's between two magic comments created by Plugman.
                var startIndex = pluginListFileContents.indexOf('TOP OF METADATA') + 15;
                var endIndex = pluginListFileContents.indexOf('// BOTTOM OF METADATA');
                var target = pluginListFileContents.substring(startIndex, endIndex);
                var metadata = JSON.parse(target);
                return metadata;
            },

            // Returns an object with plugin matching data.
            process: function(childPlugins) {
                var results = {
                    raw: childPlugins,
                    matched: [],
                    missing: [],
                    newer: [], // Those dependencies which are newer in the child than the harness.
                    older: [],  // And those which are older in the child than the harness.
                    all: []
                };

                Object.keys(childPlugins).forEach(function(plugin) {
                    var version = childPlugins[plugin];
                    results.all.push({ id: plugin, version: version });
                    if (!harnessPluginMetadata[plugin]) {
                        results.missing.push({ id: plugin, version: version });
                    } else {
                        switch(semverCompare(harnessPluginMetadata[plugin], version)) {
                            case -1: // Child older.
                                results.older.push({ id: plugin, versions: { harness: harnessPluginMetadata[plugin], child: version } });
                                break;
                            case 1: // Child newer.
                                results.newer.push({ id: plugin, versions: { harness: harnessPluginMetadata[plugin], child: version } });
                                break;
                            case 0: // Match!
                                results.matched.push({ id: plugin, version: harnessPluginMetadata[plugin] });
                                break;
                        }
                    }
                });

                return results;
            },
            // This creates the contents for the app's cordova_plugins.js file.
            // Right now, it contains the harness's plugins with all plugins not listed
            // in the target app removed.
            // TODO: is to also add in plugin .js that is exists in the app but *not*
            // in the harness. This will allow for JS-only plugins to work.
            createNewPluginListFile: function(appPluginIds) {
                function startsWith(a, b) {
                    return a.lastIndexOf(b, 0) === 0;
                }

                function isPluginIdEnabled(id) {
                    for (var i = 0; i < appPluginIds.length; ++i) {
                        if (startsWith(id, appPluginIds[i])) {
                            return true;
                        }
                    }
                    return false;
                }
                var newPluginList = harnessPluginList.filter(function(entry) {
                    return isPluginIdEnabled(entry.id);
                });
                var newMetadata = {};
                for (var i = 0; i < appPluginIds.length; ++i) {
                    var pluginId = appPluginIds[i];
                    if (pluginId in harnessPluginMetadata) {
                        newMetadata[pluginId] = harnessPluginMetadata[pluginId];
                    }
                }
                var ret = 'cordova.define("cordova/plugin_list", function(require, exports, module) {\n' +
                    'module.exports = ' + JSON.stringify(newPluginList, null, 4) + ';\n' +
                    'module.exports.metadata =\n' +
                    '// TOP OF METADATA\n' +
                    JSON.stringify(newMetadata, null, 4) + '\n' +
                    '// BOTTOM OF METADATA\n' +
                    '});\n';

                return ret;
            }
        };
    });
})();



<|start_filename|>webpack.config.js<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/
var path = require('path');

module.exports = {
  context: __dirname + "/www/cdvah",
  entry: {
    cca: [
        path.join('..', '..', 'src', 'cca.js'),
      ],
  },
  output: {
    // Make sure to use [name] or [id] in output.filename
    //  when using multiple entry points
    path: path.join(__dirname, 'www', 'cdvah', 'generated'),

    filename: "[name].bundle.js",
    chunkFilename: "[id].bundle.js",

    library: "[name]",
    libraryTarget: "umd",
  }
};


<|start_filename|>gulpfile.js<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

var gulp = require('gulp');
var autoprefixer = require('gulp-autoprefixer');
var gutil = require('gulp-util');
var jshint = require('gulp-jshint');
var path = require('path');
var webpack = require('webpack');
var webpackConfig = require('./webpack.config.js');

gulp.task('default', ['build-dev']);

gulp.task('watch', ['build-dev'], function() {
  gulp.watch([
      path.join('www', '**', '*'),
      'webpack.config.js',
      //'node_modules/**/*', // disabled because of https://github.com/gruntjs/grunt-contrib-watch#how-do-i-fix-the-error-emfile-too-many-opened-files
    ], ['lint', 'webpack:build-dev']);
  gulp.watch([
      path.join('src', '*.css'),
    ], ['styles']);
});

gulp.task('styles', function() {
  return gulp.src(path.join('src', 'style.css'))
      .pipe(autoprefixer({
                browsers: ['ios 7', 'android 3']
            }))
        .pipe(gulp.dest(path.join('www', 'cdvah', 'generated')));

});

gulp.task('build', ['lint', 'webpack:build', 'styles']);

gulp.task('build-dev', ['lint', 'webpack:build-dev', 'styles']);

gulp.task('lint', ['lint:app', 'lint:harness-push']);

/******************************************************************************/
/******************************************************************************/

gulp.task('lint:app', function() {
  return gulp.src([path.join('www', '**', '*.js'), path.join('!www', 'cdvah', 'js', 'libs', '*.js'), path.join('!www', 'cdvah', 'generated', '*.js')])
    .pipe(jshint())
    .pipe(jshint.reporter('default'))
    .pipe(jshint.reporter('fail'));
});

gulp.task('lint:harness-push', function() {
  return gulp.src([path.join('harness-push', '*.js'), path.join('harness-push', 'node_modules', 'chrome-app-developer-tool-client', '*.js')])
    .pipe(jshint())
    .pipe(jshint.reporter('default'))
    .pipe(jshint.reporter('fail'));
});

/******************************************************************************/
/******************************************************************************/

function createWebpackResultHandler(taskname, callback) {
  return function(err, stats) {
    if(err) throw new gutil.PluginError(taskname, err);
    gutil.log('[' + taskname + ']', stats.toString({
      colors: true
    }));
    callback();
  }
}

gulp.task('webpack:build', function(callback) {
  var myConfig = Object.create(webpackConfig);
  myConfig.plugins = (myConfig.plugins || []).concat(
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      }
    }),
    new webpack.optimize.DedupePlugin()
  );

  webpack(myConfig, createWebpackResultHandler('webpack:build', callback));
});

gulp.task('webpack:build-dev', (function() {
  var myDevConfig = Object.create(webpackConfig);
  myDevConfig.devtool = 'sourcemap';
  myDevConfig.debug = true;

  // create a single instance of the compiler to allow caching
  var devCompiler = webpack(myDevConfig);

  return function(callback) {
    devCompiler.run(createWebpackResultHandler('webpack:build-dev', callback));
  };
}()));


<|start_filename|>harness-push/package.json<|end_filename|>
{
  "name": "harness-push",
  "version": "0.0.2",
  "description": "Command-line utility for communicating with Chrome App Developer Tool.",
  "author": "The Chrome Team",
  "license": "Apache 2.0",
  "repository": {
    "type": "git",
    "url": "https://github.com/MobileChromeApps/chrome-app-harness.git"
  },
  "keywords": [
    "cordova",
    "cordova-app-harness",
    "Chrome",
    "chrome-app",
    "harness"
  ],
  "bin": {
    "chrome-harness-push": "./harness-push.js"
  },
  "dependencies": {
    "chrome-app-developer-tool-client": "~0.0",
    "nopt": "~2.2"
  }
}


<|start_filename|>harness-push/node_modules/chrome-app-developer-tool-client/chromepushsession.js<|end_filename|>
/**
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing,
  software distributed under the License is distributed on an
  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  KIND, either express or implied.  See the License for the
  specific language governing permissions and limitations
  under the License.
 */

var fs = require('fs');
var path = require('path');
var PushSession = require('./pushsession');

function ChromePushSession(harnessClient, dir) {
    PushSession.call(this, harnessClient, dir);
}
ChromePushSession.prototype = Object.create(PushSession.prototype);
ChromePushSession.prototype.constructor = ChromePushSession;

ChromePushSession.prototype.initialize = function(opts) {
    var manifestPath = path.join(this.rootDir_, 'manifest.json');
    var manifestMobilePath = path.join(this.rootDir_, 'manifest.mobile.json');
    if (fs.existsSync(manifestPath)) {
        opts = opts || {};

        // jshint evil:true
        var manifest = eval('(' + fs.readFileSync(manifestPath, 'utf8') + ')');
        var manifestMobile = fs.existsSync(manifestMobilePath) ? eval('(' + fs.readFileSync(manifestMobilePath, 'utf8') + ')') : {};
        // jshint evil:false

        var appId = manifestMobile['packageId'] || manifest['packageId'] || 'com.company.' + (manifest.name.replace(/[^a-zA-Z]/g, '') || 'YourApp');
        opts.appId = appId;
        opts.appType = 'chrome';
        opts.wwwDir = this.rootDir_;
        opts.configXmlPath = null;
        opts.skipCordovaPlugins = true;
    }
    return PushSession.prototype.initialize.call(this, opts);
};

module.exports = ChromePushSession;


<|start_filename|>www/cdvah/js/ApkPackager.js<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
(function() {
    'use strict';
    /* global myApp */
    /* global cca */
    myApp.factory('ApkPackager', ['$q', 'ResourcesLoader', function($q, ResourcesLoader) {

        function extractAppMetadata(app) {
            // TODO: Add to this the list of android permissions.
            return {
                'appName': app.getAppName(),
                'packageName': app.getConfigXmlId(),
                'versionName': app.getVersion(),
                'versionCode': app.getAndroidVersionCode()
            };
        }

        return {
            build: function(app, signingOpts, outputUrl) {
                var wwwDir = app.getWwwDir();
                var workDir = ResourcesLoader.createTmpFileUrl('.apkpackager');
                var templateUrl = 'file:///android_asset/www/apk-template';
                var execSigningOpts = {
                    'keyPassword': signingOpts.keyPassword
                };
                // TODO: Need to do post-prepare logic for locales
                return $q.when().then(function() {
                    if (signingOpts.privateKeyData) {
                        var privateKeyUrl = workDir + '/privatekey';
                        var certificateUrl = workDir + '/certificate';
                        execSigningOpts['privateKeyUrl'] = privateKeyUrl;
                        execSigningOpts['certificateUrl'] = certificateUrl;
                        return ResourcesLoader.writeFileContents(privateKeyUrl, cca.decodeBase64(signingOpts.privateKeyData))
                        .then(function() {
                            return ResourcesLoader.writeFileContents(certificateUrl, cca.decodeBase64(signingOpts.certificateData));
                        });
                    }
                    var keyStoreUrl = workDir + '/keystore';
                    execSigningOpts['keyStoreUrl'] = keyStoreUrl;
                    execSigningOpts['storePassword'] = signingOpts.storePassword;
                    execSigningOpts['keyAlias'] = signingOpts.keyAlias;
                    execSigningOpts['storeType'] = signingOpts.storeType;
                    return ResourcesLoader.writeFileContents(keyStoreUrl, cca.decodeBase64(signingOpts.storeData));
                }).then(function() {
                    return extractAppMetadata(app);
                }).then(function(appMetadata) {
                    var deferred = $q.defer();
                    cordova.exec(deferred.resolve, deferred.reject, 'APKPackager', 'packageAPK', [workDir + '/playground/', templateUrl, wwwDir, outputUrl, execSigningOpts, appMetadata]);
                    return deferred.promise;
                }).finally(function() {
                    return ResourcesLoader.delete(workDir);
                });
            }
        };
    }]);
})();


<|start_filename|>AppHarnessUI/android/AppHarnessUI.java<|end_filename|>
/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/
package org.apache.appharness;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.ConfigXmlParser;
import org.apache.cordova.CordovaActivity;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
import org.apache.cordova.CordovaWebViewImpl;
import org.apache.cordova.PluginEntry;
import org.apache.cordova.PluginResult;
import org.apache.cordova.whitelist.WhitelistPlugin;
import org.json.JSONArray;
import org.json.JSONException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import android.annotation.TargetApi;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.XmlResourceParser;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.animation.DecelerateInterpolator;

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class AppHarnessUI extends CordovaPlugin {
    private static final String LOG_TAG = "AppHarnessUI";
    ViewGroup contentView;
    CordovaWebView slaveWebView;
    CustomCordovaWebView slaveWebViewEngine;
    boolean slaveVisible;
    CallbackContext eventsCallback;

    @Override
    public void pluginInitialize() {
        contentView = (ViewGroup)cordova.getActivity().findViewById(android.R.id.content);
    }

    @Override
    public void onPause(boolean multitasking) {
        if (slaveWebView != null) {
            slaveWebView.handlePause(multitasking);
        }
    }

    @Override
    public void onResume(boolean multitasking) {
        if (slaveWebView != null) {
            slaveWebView.handleResume(multitasking);
        }
    }

    @Override
    public void onStart() {
        if (slaveWebView != null) {
            slaveWebView.handleStart();
        }
    }

    @Override
    public void onStop() {
        if (slaveWebView != null) {
            slaveWebView.handleStop();
        }
    }

    @Override
    public void onDestroy() {
        if (slaveWebView != null) {
            slaveWebView.handleDestroy();
        }
    }

    @Override
    public void onNewIntent(Intent intent) {
        if (slaveWebView != null) {
            slaveWebView.onNewIntent(intent);
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
        // TODO: implement me by passing a custom CordovaInterface to slaveWebView
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        if (slaveWebView != null) {
            slaveWebView.getPluginManager().onConfigurationChanged(newConfig);
        }
    }

    public boolean isSlaveVisible() {
        return slaveVisible;
    }

    public boolean isSlaveCreated() {
        return slaveWebView != null && slaveWebView.getView().getParent() != null;
    }

    @Override
    public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
        if ("create".equals(action)) {
            final Uri startUri = Uri.parse(args.getString(0));
            final Uri configXmlUri = Uri.parse(args.getString(1));
            final Set<String> pluginIdWhitelistAsSet = jsonArrayToSet(args.getJSONArray(2));
            final String webViewType = args.getString(3);
            this.cordova.getActivity().runOnUiThread(new Runnable() {
                public void run() {
                    create(startUri, configXmlUri, pluginIdWhitelistAsSet, webViewType, callbackContext);
                }
            });
        } else if ("reload".equals(action)) {
            final Uri startUri = Uri.parse(args.getString(0));
            final Uri configXmlUri = Uri.parse(args.getString(1));
            final Set<String> pluginIdWhitelistAsSet = jsonArrayToSet(args.getJSONArray(2));
            this.cordova.getActivity().runOnUiThread(new Runnable() {
                public void run() {
                    reload(startUri, configXmlUri, pluginIdWhitelistAsSet, callbackContext);
                }
            });
        } else if ("destroy".equals(action)) {
            this.cordova.getActivity().runOnUiThread(new Runnable() {
                public void run() {
                    destroy(callbackContext);
                }
            });
        } else if ("setVisible".equals(action)) {
            final boolean value = args.getBoolean(0);
            this.cordova.getActivity().runOnUiThread(new Runnable() {
                public void run() {
                    setSlaveVisible(value, callbackContext);
                }
            });
        } else if ("evalJs".equals(action)) {
            final String code = args.getString(0);
            this.cordova.getActivity().runOnUiThread(new Runnable() {
                public void run() {
                    evalJs(code, callbackContext);
                }
            });
        } else if ("events".equals(action)) {
            eventsCallback = callbackContext;
        } else {
            return false;
        }
        return true;
    }

    private Set<String> jsonArrayToSet(JSONArray jsonArray) throws JSONException {
        final Set<String> set = new HashSet<String>(jsonArray.length());
        for (int i = 0; i < jsonArray.length(); ++i) {
            set.add(jsonArray.getString(i));
        }
        return set;
    }

    public void sendEvent(String eventName) {
        if (eventsCallback != null) {
            PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, eventName);
            pluginResult.setKeepCallback(true);
            eventsCallback.sendPluginResult(pluginResult );
        }
    }

    private void evalJs(String code, CallbackContext callbackContext) {
        if (slaveWebViewEngine == null) {
            Log.w(LOG_TAG, "Not evaluating JS since no app is active");
        } else {
            slaveWebViewEngine.evaluateJavascript(code);
        }
        callbackContext.success();
    }

    private void create(Uri startUri, Uri configXmlUri, Set<String> pluginIdWhitelist, String webViewType, CallbackContext callbackContext) {
        CordovaActivity activity = (CordovaActivity)cordova.getActivity();

        if ("system".equals(webViewType) && slaveWebViewEngine instanceof CustomCrosswalkWebView ||
            "crosswalk".equals(webViewType) && slaveWebViewEngine instanceof CustomAndroidWebView) {
            slaveWebView.handleDestroy();
            slaveWebView = null;
            slaveWebViewEngine = null;
        }

        if (slaveWebView == null) {
            if ("system".equals(webViewType)) {
                slaveWebViewEngine = new CustomAndroidWebView(this, activity);
            } else {
                slaveWebViewEngine = new CustomCrosswalkWebView(this, activity, preferences);
            }
            slaveWebView = new CordovaWebViewImpl((CordovaWebViewEngine)slaveWebViewEngine);
            // A consistent view ID is needed for plugins that utilize the background-activity plugin.
            slaveWebView.getView().setId(200);
            // We'll set the plugin entries in initWebView.
            slaveWebView.init(cordova, new ArrayList<PluginEntry>(), preferences);
        }
        setPluginEntries(pluginIdWhitelist, configXmlUri);

        slaveWebView.clearCache(true);
        slaveWebView.clearHistory();
        slaveWebView.loadUrl(startUri.toString());
        contentView.addView(slaveWebView.getView());
        slaveVisible = true;
        // Back button capturing breaks without these:
        webView.getView().setEnabled(false);
        slaveWebView.getView().requestFocus();
        callbackContext.success();
    }

    private void reload(Uri startUri, Uri configXmlUri, Set<String> pluginIdWhitelist, CallbackContext callbackContext) {
        if (slaveWebView == null) {
            Log.w(LOG_TAG, "reload: no webview exists");
        } else {
            // TODO(maxw): If the webview type has changed, create a new webview.
            setPluginEntries(pluginIdWhitelist, configXmlUri);
            slaveWebView.clearCache(true);
            slaveWebView.clearHistory();
            slaveWebView.loadUrl(startUri.toString());
        }
        callbackContext.success();
    }

    private void destroy(CallbackContext callbackContext) {
        if (slaveWebView == null) {
            Log.w(LOG_TAG, "destroy: already destroyed");
        } else {
            slaveWebView.loadUrl("data:text/plain;charset=utf-8,");
            contentView.removeView(slaveWebView.getView());
            webView.getView().setEnabled(true);
            webView.getView().requestFocus();

            slaveWebView.getView().setScaleX(1.0f);
            slaveWebView.getView().setScaleY(1.0f);
            slaveWebViewEngine.setStealTapEvents(false);
            slaveVisible = false;
            sendEvent("destroyed");
        }
        if (eventsCallback != null) {
            eventsCallback.success("");
            eventsCallback = null;
        }
        callbackContext.success();
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void setSlaveVisible(boolean value, CallbackContext callbackContext) {
        if (value == slaveVisible) {
            return;
        }
        if (slaveWebView == null) {
            Log.w(LOG_TAG, "setSlaveVisible: slave not created");
        } else {
            slaveVisible = value;
            ViewPropertyAnimator anim = slaveWebView.getView().animate();
            // Note: Pivot is set in onSizeChanged.
            if (value) {
                anim.scaleX(1.0f).scaleY(1.0f);
                webView.getView().setEnabled(false);
                slaveWebView.getView().setEnabled(true);
                slaveWebView.getView().requestFocus();
            } else {
                anim.scaleX(.25f).scaleY(.25f);
                webView.getView().setEnabled(true);
                slaveWebView.getView().setEnabled(false);
                webView.getView().requestFocus();
            }
            slaveWebViewEngine.setStealTapEvents(!value);
            anim.setDuration(300).setInterpolator(new DecelerateInterpolator(2.0f)).start();
        }
        if (callbackContext != null) {
            callbackContext.success();
        }
    }

    private void setPluginEntries(Set<String> pluginIdWhitelist, Uri configXmlUri) {
        CordovaActivity activity = (CordovaActivity)cordova.getActivity();
        // Extract the <feature> from CADT's config.xml, and filter out unwanted plugins.
        ConfigXmlParser parser = new ConfigXmlParser();
        parser.parse(activity);
        ArrayList<PluginEntry> pluginEntries = new ArrayList<PluginEntry>(parser.getPluginEntries());
        for (int i = 0; i < pluginEntries.size();) {
            PluginEntry p = pluginEntries.get(i);
            if (!pluginIdWhitelist.contains(p.service)) {
                pluginEntries.remove(p);
                continue;
            } else if (WhitelistPlugin.class.getCanonicalName().equals(p.pluginClass)) {
                pluginEntries.set(i, new PluginEntry(p.service, createWhitelistPlugin(configXmlUri)));
            }
            ++i;
        }
        slaveWebView.getPluginManager().setPluginEntries(pluginEntries);
        // This is added by cordova-android in code, so we need to re-add it likewise.
        // Note that we re-route navigator.app.exitApp() in JS to close the webview rather than close the Activity.
        slaveWebView.getPluginManager().addService("CoreAndroid", "org.apache.cordova.CoreAndroid");
    }

    private CordovaPlugin createWhitelistPlugin(Uri configXmlUri) {
        InputStream istr = null;
        try {
            istr = new FileInputStream(configXmlUri.getPath());
            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
            factory.setNamespaceAware(false);
            XmlPullParser parser = factory.newPullParser();
            parser.setInput(istr, "UTF-8");
            return new WhitelistPlugin(parser);
        } catch (XmlPullParserException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (istr != null) {
                try {
                    istr.close();
                } catch (IOException e) {
                }
            }
        }
        return null;
    }
}


<|start_filename|>harness-push/node_modules/chrome-app-developer-tool-client/package.json<|end_filename|>
{
  "name": "chrome-app-developer-tool-client",
  "version": "0.0.6-dev",
  "description": "Client library for communicating with Chrome App Developer Tool for Mobile.",
  "main": "chromeharnessclient.js",
  "author": "The <NAME>",
  "license": "Apache 2.0",
  "repository": {
    "type": "git",
    "url": "https://github.com/MobileChromeApps/chrome-app-developer-tool.git"
  },
  "keywords": [
    "cordova",
    "cordova-app-harness",
    "cca",
    "chrome-app-developer-tool",
    "Chrome",
    "chrome-app",
    "harness"
  ],
  "dependencies": {
    "adbkit": "^2.1.2",
    "agentkeepalive": "^1.2.0",
    "jszip": "~2.1",
    "old-agentkeepalive": "^0.2.2",
    "q": "~0.9",
    "shelljs": "0.1.x",
    "temp": "~0.7"
  }
}


<|start_filename|>www/cdvah/js/AppsService.js<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
(function() {
    'use strict';
    /* global myApp */
    myApp.factory('AppsService', ['$q', 'ResourcesLoader', 'INSTALL_DIRECTORY', 'APPS_JSON', 'AppHarnessUI', 'Reporter', function($q, ResourcesLoader, INSTALL_DIRECTORY, APPS_JSON, AppHarnessUI, Reporter) {
        // Map of type -> installer.
        var _installerFactories = Object.create(null);
        // Array of installer objects.
        var _installers = null;
        // The app that is currently running.
        var activeInstaller = null;
        var lastAccessedInstaller = null;
        var curWebViewType = null;

        function readAppsJson() {
            var deferred = $q.defer();
            ResourcesLoader.readJSONFileContents(APPS_JSON)
            .then(function(result) {
                if (result['fileVersion'] !== 1) {
                    console.warn('Ignoring old version of apps.json');
                    result = {};
                }
                deferred.resolve(result);
            }, function() {
                // Error means first run.
                deferred.resolve({});
            });
            return deferred.promise;
        }

        function initHandlers() {
            if (_installers) {
                return $q.when();
            }

            return readAppsJson()
            .then(function(json) {
                var appList = json['appList'] || [];
                _installers = [];
                var i = -1;
                return $q.when()
                .then(function next() {
                    var entry = appList[++i];
                    if (!entry) {
                        return;
                    }
                    var Ctor = _installerFactories[entry['appType']];
                    return new Ctor().initFromJson(entry)
                    .then(function(app) {
                        _installers.push(app);
                        return next();
                    }, next);
                })
                .then(function() {
                    lastAccessedInstaller = _installers.filter(function(x) { return x.appId === json['lastAccessedAppId']; })[0] || null;
                });
            });
        }

        function createAppsJson() {
            var appsJson = {
                'fileVersion': 1,
                'lastAccessedAppId': lastAccessedInstaller ? lastAccessedInstaller.appId : null,
                'appList': []
            };
            for (var i = 0; i < _installers.length; ++i) {
                var installer = _installers[i];
                appsJson.appList.push(installer.toDiskJson());
            }
            return appsJson;
        }

        function writeAppsJson() {
            if (AppsService.onAppListChange) {
                AppsService.onAppListChange();
            }
            var appsJson = createAppsJson();
            var stringContents = JSON.stringify(appsJson, null, 4);
            return ResourcesLoader.writeFileContents(APPS_JSON, stringContents);
        }

        function updateLastAccessed(app) {
            if (lastAccessedInstaller != app) {
                lastAccessedInstaller = app;
                return writeAppsJson();
            }
            return $q.when();
        }

        var AppsService = {
            // return promise with the array of apps
            getAppList : function() {
                return initHandlers()
                .then(function() {
                    return _installers.slice();
                });
            },

            getLastAccessedApp: function() {
                return lastAccessedInstaller;
            },

            getActiveApp: function() {
                return activeInstaller;
            },

            getAppListAsJson : function() {
                return createAppsJson();
            },

            // If no appId, then return the first app.
            // If appId and appType, then create it if it doesn't exist.
            // Else: return null.
            getAppById : function(appId, /* optional */ appType) {
                return initHandlers()
                .then(function() {
                    var matches = _installers;
                    if (appId) {
                        matches = _installers.filter(function(x) {
                            return x.appId == appId;
                        });
                    }
                    if (matches.length > 0) {
                        if (appType) {
                            return updateLastAccessed(matches[0])
                            .then(function() {
                                return matches[0];
                            });
                        }
                        return matches[0];
                    }
                    if (appType) {
                        return AppsService.addApp(appType, appId);
                    }
                    return null;
                });
            },

            quitApp : function() {
                if (activeInstaller) {
                    activeInstaller.unlaunch();
                    activeInstaller = null;
                    curWebViewType = null;
                    return AppHarnessUI.destroy();
                }
                return $q.when();
            },

            launchApp : function(installer) {
                // Determine whether we're relaunching the same app as is already active.
                var activeAppId = activeInstaller && activeInstaller.appId;
                var newAppId = installer && installer.appId;
                var relaunch = activeAppId && newAppId && activeAppId === newAppId;

                return $q.when()
                .then(function() {
                    // If we're relaunching the active app, move on.
                    // Otherwise, quit the active app.
                    // TODO(maxw): Determine whether we actually ever need to quit the app.
                    if (relaunch) {
                        return $q.when();
                    } else {
                        return AppsService.quitApp();
                    }
                }).then(function() {
                    activeInstaller = installer;
                    return updateLastAccessed(installer);
                }).then(function() {
                    return installer.launch();
                }).then(function(launchUrl) {
                    return installer.getPluginMetadata()
                    .then(function(pluginMetadata) {
                        var configXmlUrl = installer.directoryManager.rootURL + 'config.xml';
                        var webViewType = installer.getWebViewType();
                        if (relaunch) {
                            if (webViewType != curWebViewType) {
                                curWebViewType = webViewType;
                                return AppHarnessUI.destroy()
                                .then(function() {
                                    return AppHarnessUI.create(launchUrl, configXmlUrl, pluginMetadata, webViewType);
                                });
                            } else {
                                return AppHarnessUI.reload(launchUrl, configXmlUrl, pluginMetadata);
                            }
                        } else {
                            curWebViewType = webViewType;
                            return AppHarnessUI.create(launchUrl, configXmlUrl, pluginMetadata, webViewType);
                        }
                    }).then(function() {
                        if (AppsService.onAppListChange) {
                            AppsService.onAppListChange();
                        }
                    }).then(function() {
                        Reporter.sendEvent('app', 'launched');
                    });
                });
            },

            addApp : function(appType, /* optional */ appId) {
                var installPath = INSTALL_DIRECTORY + 'app' + Math.floor(Math.random() * 0xFFFFFFFF).toString(36) + '/';
                return initHandlers().then(function() {
                    var Ctor = _installerFactories[appType];
                    return new Ctor().init(installPath, appId);
                }).then(function(installer) {
                    _installers.push(installer);
                    lastAccessedInstaller = installer;
                    return writeAppsJson()
                    .then(function() {
                        return installer;
                    });
                });
            },

            uninstallAllApps : function() {
                return this.quitApp()
                .then(function() {
                    var deletePromises = [];
                    for (var i = 0; i < _installers.length; ++i) {
                        deletePromises.push(AppsService.uninstallApp(_installers[i]));
                    }
                    return $q.all(deletePromises);
                });
            },

            uninstallApp : function(installer) {
                var ret = $q.when();
                if (lastAccessedInstaller == installer) {
                    lastAccessedInstaller = null;
                }
                if (installer == activeInstaller) {
                    ret = this.quitApp();
                }
                return ret.then(function() {
                    return installer.deleteFiles();
                }).then(function() {
                    _installers.splice(_installers.indexOf(installer), 1);
                    return writeAppsJson();
                });
            },

            triggerAppListChange: function() {
                return writeAppsJson();
            },

            registerInstallerFactory : function(installerFactory) {
                 _installerFactories[installerFactory.type] = installerFactory;
            },

            onAppListChange: null
        };
        return AppsService;
    }]);
})();


<|start_filename|>www/cdvah/js/CrxInstaller.js<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
(function(){
    'use strict';
    /* global cca */

    // The only things that matters here at the moment
    // are the appId and <content>.
    var CONFIG_XML_TEMPLATE = '<?xml version="1.0" encoding="UTF-8"?>\n' +
        '<widget>\n' +
        '<name></name>\n' +
        '<description></description>\n' +
        '<author></author>\n' +
        '<preference name="KeyboardShrinksView" value="true" />\n' +
        '<preference name="StatusBarOverlaysWebView" value="false" />\n' +
        '<preference name="StatusBarBackgroundColor" value="#000000" />\n' +
        '<preference name="iosPersistentFileLocation" value="Library" />\n' +
        '<preference name="AndroidPersistentFileLocation" value="Internal" />\n' +
        '</widget>\n';


    function generateConfigXmlData(manifest) {
        var template = CONFIG_XML_TEMPLATE;
        var analyzedManifest = cca.analyseManifest(manifest);
        var configXmlDom = new DOMParser().parseFromString(template, 'text/xml');
        cca.updateConfigXml(manifest, analyzedManifest, configXmlDom);
        return new XMLSerializer().serializeToString(configXmlDom);
    }

    /* global myApp */
    myApp.factory('CrxInstaller', ['$q', 'Installer', 'AppsService', 'ResourcesLoader', 'pluginDepsMap', function($q, Installer, AppsService, ResourcesLoader, pluginDepsMap) {

        function CrxInstaller() {
            this.mergedManifestJson_ = null;
        }
        CrxInstaller.prototype = Object.create(Installer.prototype);
        CrxInstaller.prototype.constructor = CrxInstaller;
        CrxInstaller.type = 'chrome';

        CrxInstaller.prototype.initFromJson = function(json) {
            var self = this;
            return Installer.prototype.initFromJson.call(this, json)
            .then(function() {
                return self.readManifest_();
            }).then(function() {
                return self.updateDerivedFiles();
            }).then(function() {
                return self;
            }, function(e) {
                console.warn('Deleting broken app: ' + json['installPath']);
                ResourcesLoader.delete(json['installPath']);
                throw e;
            });
        };

        CrxInstaller.prototype.onFileAdded = function(path, etag) {
            var self = this;
            return $q.when(Installer.prototype.onFileAdded.call(this, path, etag))
            .then(function() {
                if (path == 'www/manifest.json' || (path == 'www/manifest.mobile.json' && self.mergedManifestJson_)) {
                    return self.readManifest_()
                    .then(function() {
                        return self.updateDerivedFiles();
                    });
                }
            });
        };

        CrxInstaller.prototype.readManifest_ = function() {
            var self = this;
            return cca.parseAndMergeManifests(this.directoryManager.rootURL + 'www/manifest.json', 'android', ResourcesLoader.readFileContents, $q)
            .then(function(m) {
                self.mergedManifestJson_ = m;
            });
        };

        CrxInstaller.prototype.updateDerivedFiles = function() {
            var self = this;
            var contents = generateConfigXmlData(this.mergedManifestJson_);
            var combinedEtag = this.directoryManager.getAssetEtag('www/manifest.json') + this.directoryManager.getAssetEtag('www/manifest.mobile.json');
            return this.directoryManager.writeFile(contents, 'config.xml', combinedEtag)
            .then(function() {
                return self.updateCordovaPluginsFile(self.directoryManager.getAssetEtag('www/manifest.json'));
            });
        };

        function expandPluginIdsWithDeps(ids) {
            var idMap = {};
            function addAll(arr) {
                arr.forEach(function(pluginId) {
                    pluginId = pluginId.replace(/@.*/, '');
                    if (!idMap[pluginId]) {
                        idMap[pluginId] = true;
                        var deps = pluginDepsMap[pluginId];
                        if (deps) {
                            addAll(deps);
                        }
                    }
                });
            }
            addAll(ids);
            return Object.keys(idMap);
        }

        CrxInstaller.prototype.getPluginMetadata = function() {
            var pluginIds = cca.analyseManifest(this.mergedManifestJson_).pluginsToBeInstalled;
            pluginIds = expandPluginIdsWithDeps(pluginIds);
            var harnessPluginMetadata = cordova.require('cordova/plugin_list').metadata;
            var ret = {};
            // Make all versions match what is installed.
            for (var i = 0; i < pluginIds.length; ++i) {
                ret[pluginIds[i]] = harnessPluginMetadata[pluginIds[i]] || '0';
            }
            return $q.when(ret);
        };

        function injectCsp(htmlPath, cspTag) {
            return ResourcesLoader.readFileContents(htmlPath)
            .then(function(html) {
                html = html.replace(/<meta.*Content-Security.*>/, cspTag);
                return ResourcesLoader.writeFileContents(htmlPath, html);
            });
        }

        CrxInstaller.prototype.launch = function() {
            var appWwwUrl = this.getWwwDir();
            var cspContent = cca.analyseManifest.createCspString(this.mergedManifestJson_, cordova.platformId);
            var cspTag = '<meta http-equiv="Content-Security-Policy" content="' + cspContent + '">';
            return Installer.prototype.launch.call(this)
            .then(function(ret) {
                return injectCsp(appWwwUrl + 'plugins/cordova-plugin-chrome-apps-bootstrap//chromeapp.html', cspTag)
                .then(function() {
                    return injectCsp(appWwwUrl + 'plugins/cordova-plugin-chrome-apps-bootstrap//chromebgpage.html', cspTag);
                }).then(function() {
                    return ret;
                });
            });
        };

        return CrxInstaller;
    }]);

    myApp.run(['CrxInstaller', 'AppsService', function(CrxInstaller, AppsService) {
        AppsService.registerInstallerFactory(CrxInstaller);
    }]);
})();


<|start_filename|>src/style.css<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/

@font-face {
    font-family: 'Roboto Condensed';
    font-style: normal;
    font-weight: 400;
    src: local('Roboto Condensed Regular'), local('RobotoCondensed-Regular'), url(../css/Zd2E9abXLFGSr9G3YK2MsNxB8OB85xaNTJvVSB9YUjQ.woff) format('woff');
}

* {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}

img {
    border: none;
    margin: 0;
}

a:link, a:visited, a:hover, a:active {
    /* text-decoration:none; */
    color: #fff;
}

h1, h2, h3 {
  text-align: center;
  width: 100%; /* applies when in an hflex-container */
}

ul {
  text-align: left;
}

html {
    background: #222;
    color: #ddd;
    font-family: "Roboto Condensed";
    text-align: center;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
    /* For large screens */
    padding: 30px 30px 0 30px;
    font-size: 14pt;
}

@media all and (max-width: 500px) {
    html {
        padding: 20px 8px 0 8px;
        font-size: 10pt;
    }
}

body {
    margin: 0;
}

html, body {
    height: 100%;
    position: relative;
    max-width: 100%;
    overflow-x: hidden;
}

.vflex-container {
    display: flex;
    flex-direction: column;
}

.hflex-container {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
}

.flex-justify-between {
    justify-content: space-between;
}

.flex-justify-around {
    justify-content: space-around;
}

.flex-1 {
    flex-grow: 1;
    flex-basis: 0;
}

.scrollable {
    overflow: auto;
    -webkit-overflow-scrolling: touch;
}

.title {
    font-size: 1.8rem;
    margin-bottom: 10px;
}

/* iOS title */
@media all and (max-width: 330px) {
    .title {
        font-size: 1.5rem;
    }
}

.title-version {
    font-size: .6em;
}

.big-button {
    font-size: 1rem;
}

.listening-container {
    margin-bottom: 10pt;
    font-size: 1.2rem;
}

.listening-ip {
    color: #AAA;
    margin: 0 3pt;
    display: inline-block;
}

.listening-error {
    color: #E00;
}

.listening-port {
    color: #0A0;
}

.app-container {
    border: 1px solid #555;
    border-radius: 2px;
    text-align: left;
    position: relative;
    padding: .5rem;
    background: #444;
    width: 80%;
    line-height: 1.5rem;
}

@media all and (max-width: 500px) {
    .app-container {
        width: 90%;
    }
}

.app-empty-message {
    font-size: 1.5rem;
}

.app-button-x {
    position: absolute;
    top: -1.2rem;
    cursor:pointer;
    color: #fff;
    border-radius: 1.2rem;
    background: #D13D3D;
    display: inline-block;
    padding: 1.2rem;
    left: -1.2rem;
}

.app-button-x::after {
    content: "\d7";
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    line-height: 2.2rem;
    font-size: 2rem;
    text-align: center;
}

.app-icon-container {
    margin-right: 6px;
}

.app-icon {
    width: 64px;
    height: 64px;
    line-height: 64px;
    display: inline-block;
    vertical-align: middle;
}

.app-icon-missing {
    text-align: center;
    border: 1px solid #666;
}

@media all and (max-width: 500px) {
    .app-icon {
        width: 48px;
        height: 48px;
        line-height: 48px;
    }
}

@media all and (max-width: 500px) {
    .app-text-container-1 {
        left: 58px;
    }
}

.app-title {
    font-size: 1.3rem;
    line-height: 2rem;
}

.app-version {
}

.app-last-updated {
    color: #ccc;
}

.app-plugin-status {
    color: #ccc;
}

.app-button-container {
    text-align: center;
    margin-top: 12px;
}

.app-button {
    background-color: #c32;
    border:1px solid #d02718;
    display:inline-block;
    color: #ddd;
    font-size: 15px;
    font-weight:bold;
    padding: 4px 12px;
    -webkit-tap-highlight-color: rgba(0,0,0,0);
}
.app-button:active {
    background-color: #a32;
}

.footer {
    margin: 2.2rem 0;
}

.plugin-newer {
  color: #FFCA21;
}
.plugin-older {
  color: #FFCA21;
}
.plugin-missing {
  color: #1ADAFF;
}

.plugin-newer>span, .plugin-older>span, .plugin-missing>span {
  font-style: italic;
}


<|start_filename|>www/cdvah/js/AboutCtrl.js<|end_filename|>
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
*/
(function(){
    'use strict';
    /* global analytics */
    /* global myApp */
    myApp.controller('AboutCtrl', ['$rootScope', '$scope', '$location', 'PluginMetadata', 'Reporter', function($rootScope, $scope, $location, PluginMetadata, Reporter) {
        // Track the page view.
        Reporter.sendPageView('about');

        $scope.plugins = PluginMetadata.availablePlugins().filter(function(p) {
            return !/UrlRemap|appharness/.exec(p.id);
        });

        $scope.goBack = function() {
            if ($scope.config) {
                $scope.config.setTrackingPermitted($scope.formData.reportingPermissionCheckbox);
            }
            $location.path('/');
        };

        var getConfigCallback = function(config) {
            // Save the config object so we can update it when the user leaves this screen.
            $scope.config = config;

            // Initialize the reporting permission checkbox according to the recorded response.
            var permitted = config.isTrackingPermitted();
            $scope.formData = { reportingPermissionCheckbox: permitted };
        };

        // Get the config object.
        var service = analytics.getService($rootScope.appTitle);
        service.getConfig().addCallback(getConfigCallback);
    }]);
})();


<|start_filename|>package.json<|end_filename|>
{
  "name": "chrome-app-developer-tool",
  "version": "0.13.1-dev",
  "description": "The Chrome App Developer Tool for Mobile (\"App Dev Tool\" for short) is a distribution of [Apache Cordova App Harness](https://git-wip-us.apache.org/repos/asf/cordova-app-harness.git) that can run Chrome Apps. It is based on the plugins from the [cca](https://github.com/MobileChomeApps/mobile-chrome-apps) toolkit.",
  "repository": {
    "type": "git",
    "url": "git://github.com/MobileChromeApps/chrome-app-developer-tool.git"
  },
  "author": "The Chrome Team",
  "license": "Apache version 2.0",
  "bugs": {
    "url": "https://github.com/MobileChromeApps/chrome-app-developer-tool/issues"
  },
  "homepage": "https://github.com/MobileChromeApps/chrome-app-developer-tool",
  "devDependencies": {
    "base64-arraybuffer": "^0.1.2",
    "cca": "latest",
    "cca-manifest-logic": "^1.1.1",
    "cordova": "latest",
    "gulp": "latest",
    "gulp-autoprefixer": "^1.0.1",
    "gulp-jshint": "~1.6",
    "gulp-util": "latest",
    "webpack": "latest"
  }
}


<|start_filename|>AppHarnessUI/android/CustomCrosswalkWebView.java<|end_filename|>
/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/
package org.apache.appharness;

import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaWebViewEngine;
import org.crosswalk.engine.XWalkCordovaView;
import org.crosswalk.engine.XWalkWebViewEngine;

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;

class CustomCrosswalkWebView extends XWalkWebViewEngine implements CustomCordovaWebView {
    private static final String LOG_TAG = "AppHarnessUI";

    private AppHarnessUI parent;
    private boolean stealTapEvents;
    private TwoFingerDoubleTapGestureDetector twoFingerTapDetector;

    CustomCrosswalkWebView(AppHarnessUI parent, Context context, CordovaPreferences preferences) {
        super(new CustomXwalkView(context, preferences));
        this.parent = parent;
        ((CustomXwalkView)webView).parent = this;
        twoFingerTapDetector = new TwoFingerDoubleTapGestureDetector(parent);
    }

    @Override
    public void setStealTapEvents(boolean value){
        stealTapEvents=value;
    }

    @Override
    public void evaluateJavascript(String script) {
        webView.evaluateJavascript(script, null);
    }

    @Override
    public CordovaWebViewEngine asEngine() {
        return this;
    }

    @Override
    public boolean goBack() {
        if (webView.getNavigationHistory().canGoBack()) {
            return super.goBack();
        }
        if (parent.slaveVisible) {
            parent.sendEvent("showMenu");
            return true;
        }
        // Should never get here since the webview does not have focus.
        Log.w(LOG_TAG, "Somehow back button was pressed when app not visible");
        return false;
    }

    private static class CustomXwalkView extends XWalkCordovaView {
        CustomCrosswalkWebView parent;
        public CustomXwalkView(Context context, CordovaPreferences preferences) {
            super(context, preferences);
        }

        @Override
        public boolean onTouchEvent(MotionEvent e) {
            if (parent.stealTapEvents) {
                if (e.getAction() == MotionEvent.ACTION_UP) {
                    parent.parent.sendEvent("hideMenu");
                }
                return true;
            }
            return super.onTouchEvent(e);
        }
        @Override
        public boolean onInterceptTouchEvent(MotionEvent e) {
            if (parent.stealTapEvents) {
                if (e.getAction() == MotionEvent.ACTION_UP) {
                    parent.parent.sendEvent("hideMenu");
                }
                return true;
            }
            parent.twoFingerTapDetector.onTouchEvent(e);
            return super.onInterceptTouchEvent(e);
        }

        @SuppressLint("NewApi")
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            // Needed for the view to stay in the bottom when rotating.
            setPivotY(h);
        }
    }

}


<|start_filename|>harness-push/node_modules/chrome-app-developer-tool-client/chromeharnessclient.js<|end_filename|>
/**
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing,
  software distributed under the License is distributed on an
  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  KIND, either express or implied.  See the License for the
  specific language governing permissions and limitations
  under the License.
 */
var fs = require('fs');
var HarnessClient = require('./harnessclient');
var ChromePushSession = require('./chromepushsession');

function ChromeHarnessClient(target) {
    HarnessClient.call(this, target);
}
ChromeHarnessClient.detectAdbTargets = HarnessClient.detectAdbTargets;
ChromeHarnessClient.prototype = Object.create(HarnessClient.prototype);
ChromeHarnessClient.prototype.constructor = ChromeHarnessClient;

ChromeHarnessClient.prototype.createPushSession = function(dir) {
    return new ChromePushSession(this, dir);
};

ChromeHarnessClient.prototype.buildApk = function(appId, appType, signingOpts, outputPath, /* optional */ manifestEtag) {
    var query = {};
    if (manifestEtag) {
        query['manifestEtag'] = manifestEtag;
    }
    var payload = {
        'keyPassword': signingOpts.keyPassword
    };
    if (signingOpts.privateKeyPath) {
        payload['publicKeyData'] = fs.readFileSync(signingOpts.certificatePath, 'base64');
        payload['privateKeyData'] = fs.readFileSync(signingOpts.privateKeyPath, 'base64');
    } else {
        payload['storeData'] = fs.readFileSync(signingOpts.keyStorePath, 'base64');
        payload['storeType'] = signingOpts.storeType;
        payload['storePassword'] = <PASSWORD>Opts.storePassword;
        payload['keyAlias'] = signingOpts.keyAlias;
    }
    var opts = {
        appId: appId,
        appType: appType,
        json: payload,
        query: query,
        saveResponseToFile: outputPath
    };
    return this.doRequest('POST', 'buildapk', opts);
};

module.exports = ChromeHarnessClient;



<|start_filename|>www/cdvah/views/details.html<|end_filename|>
<!--
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing,
  software distributed under the License is distributed on an
  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  KIND, either express or implied.  See the License for the
  specific language governing permissions and limitations
  under the License.
-->
<div class="title">{{appTitle}} <span class="title-version">v{{appVersion}}</span></div>
<h2>{{ app.appId }}</h2>
<div class="flex-1 scrollable" style="text-align:left">
    <h3>Plugins in Use: {{ app.plugins.matched.length + app.plugins.older.length + app.plugins.newer.length }}</h3>
    <ul style="overflow:hidden">
        <li ng-repeat="plugin in app.plugins.matched">
            <strong>{{ plugin.id }}</strong>: {{ plugin.version }}
        </li>
        <li ng-repeat="plugin in app.plugins.newer" class="plugin-newer">
            <strong>{{ plugin.id }}</strong>: {{ plugin.versions.harness}}
            <span>(wants {{ plugin.versions.child }})</span>
        </li>
        <li ng-repeat="plugin in app.plugins.older" class="plugin-older">
            <strong>{{ plugin.id }}</strong>: {{ plugin.versions.harness}}
            <span>(wants {{ plugin.versions.child }})</span>
        </li>
        <li ng-repeat="plugin in app.plugins.missing" class="plugin-missing">
            <strong>{{ plugin.id }}</strong>
            <span>(missing)</span>
        </li>
    </ul>
    <div ng-show="app.plugins.missing">
        Missing plugins require that you build {{appTitle}} from source with these extra plugins added. See the <a href="https://github.com/MobileChromeApps/chrome-app-developer-tool/blob/master/README.md">README</a> for details.
    </div>
    <div>
        Plugin problems can sometimes be resolved by updating {{appTitle}}, or by it from source. See the <a href="https://github.com/MobileChromeApps/chrome-app-developer-tool/blob/master/README.md">README</a> for details.
    </div>
</div>
<div style="margin:20px">
    <button ng-click="goBack()" class="big-button">Done</button>
</div>


<|start_filename|>www/cdvah/views/permission.html<|end_filename|>
<!--
  Licensed to the Apache Software Foundation (ASF) under one
  or more contributor license agreements.  See the NOTICE file
  distributed with this work for additional information
  regarding copyright ownership.  The ASF licenses this file
  to you under the Apache License, Version 2.0 (the
  "License"); you may not use this file except in compliance
  with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing,
  software distributed under the License is distributed on an
  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  KIND, either express or implied.  See the License for the
  specific language governing permissions and limitations
  under the License.
-->

<div class="title">{{appTitle}} <span class="title-version">v{{appVersion}}</span></div>
<div class="vflex-container flex-justify-around flex-1" style="font-size:1.15rem; margin:0 24px">
<div>
  <h3>Welcome!</h3>
  <p>The Chrome App Developer Tool aids in the development of Chrome Apps for Mobile. It can be used to run Chrome Apps for Mobile with the Chrome Dev Editor, or with the cca tool, for faster development and easier debugging.</p>
</div>
<div class="hflex-container">
  <h3>Usage Statistics and Crash Reports</h3>
  <input type="checkbox" style="width:16px;height:16px" ng-model="formData.reportingPermissionCheckbox">
  <div class="flex-1" style="text-align:justify;margin:0 4px 0 14px">Help make {{appTitle}} better by automatically sending usage statistics and crash reports to Google. This information will be used in accordance with the <a href="http://www.google.com/policies/privacy/">Google Privacy Policy</a>.
  </div>
</div>
</div>
<div style="margin:20px">
    <button ng-click="saveReportingPermission()" class="big-button">Get Started</button>
</div>
