[
  {
    "path": ".gitignore",
    "content": "#ignore these files\n.DS_Store"
  },
  {
    "path": "Android/ClientCode/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  - Copyright 2012 Google Inc.\n  -\n  - Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n  - use this file except in compliance with the License. You may obtain a copy\n  - of the License at\n  -\n  - http://www.apache.org/licenses/LICENSE-2.0\n  -\n  - Unless required by applicable law or agreed to in writing, software\n  - distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n  - WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n  - License for the specific language governing permissions and limitations\n  - under the License.\n-->\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"com.google.android.gcm.demo.app\"\n    android:versionCode=\"1\"\n    android:versionName=\"1.0\" >\n\n    <!-- GCM requires Android SDK version 2.2 (API level 8) or above. -->\n    <!--\n         The targetSdkVersion is optional, but it's always a good practice\n         to target higher versions.\n    -->\n    <uses-sdk\n        android:minSdkVersion=\"8\"\n        android:targetSdkVersion=\"16\" />\n\n    <!-- GCM connects to Google Services. -->\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <!-- GCM requires a Google account. -->\n    <uses-permission android:name=\"android.permission.GET_ACCOUNTS\" />\n\n    <!-- Keeps the processor from sleeping when a message is received. -->\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n\n    <!--\n     Creates a custom permission so only this app can receive its messages.\n\n     NOTE: the permission *must* be called PACKAGE.permission.C2D_MESSAGE,\n           where PACKAGE is the application's package name.\n    -->\n    <permission\n        android:name=\"com.google.android.gcm.demo.app.permission.C2D_MESSAGE\"\n        android:protectionLevel=\"signature\" />\n\n    <uses-permission android:name=\"com.google.android.gcm.demo.app.permission.C2D_MESSAGE\" />\n\n    <!-- This app has permission to register and receive data message. -->\n    <uses-permission android:name=\"com.google.android.c2dm.permission.RECEIVE\" />\n\n    <!-- Main activity. -->\n    <application\n        android:icon=\"@drawable/ic_launcher\"\n        android:label=\"@string/app_name\" >\n        <activity\n            android:name=\"com.google.android.gcm.demo.app.DemoActivity\"\n            android:label=\"@string/app_name\"\n            android:screenOrientation=\"portrait\" >\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n        <!--\n          BroadcastReceiver that will receive intents from GCM\n          services and handle them to the custom IntentService.\n\n          The com.google.android.c2dm.permission.SEND permission is necessary\n          so only GCM services can send data messages for the app.\n        -->\n        <receiver\n            android:name=\"com.google.android.gcm.GCMBroadcastReceiver\"\n            android:permission=\"com.google.android.c2dm.permission.SEND\" >\n            <intent-filter>\n\n                <!-- Receives the actual messages. -->\n                <action android:name=\"com.google.android.c2dm.intent.RECEIVE\" />\n                <!-- Receives the registration id. -->\n                <action android:name=\"com.google.android.c2dm.intent.REGISTRATION\" />\n\n                <category android:name=\"com.google.android.gcm.demo.app\" />\n            </intent-filter>\n        </receiver>\n\n        <!--\n          Application-specific subclass of GCMBaseIntentService that will\n          handle received messages.\n\n          By default, it must be named .GCMIntentService, unless the\n          application uses a custom BroadcastReceiver that redefines its name.\n        -->\n        <service android:name=\"com.google.android.gcm.demo.app.GCMIntentService\" />\n\n    </application>\n\n</manifest>"
  },
  {
    "path": "Android/ClientCode/README",
    "content": "GCM demo app\n------------\n\n1.To create the project settings, type:\n\n  android update project --name GCMDemo -p . --target android-16\n\n2.To build the app, type:\n\n  ant clean debug\n\n(but set SENDER_ID and SERVER_URL on /src/com/google/android/gcm/demo/app/CommonUtilities.java first)\n\n3.To install the app, type\n\n  ant installd\n"
  },
  {
    "path": "Android/ClientCode/gen/com/google/android/gcm/demo/app/BuildConfig.java",
    "content": "/** Automatically generated file. DO NOT MODIFY */\npackage com.google.android.gcm.demo.app;\n\npublic final class BuildConfig {\n    public final static boolean DEBUG = true;\n}"
  },
  {
    "path": "Android/ClientCode/gen/com/google/android/gcm/demo/app/Manifest.java",
    "content": "/* AUTO-GENERATED FILE.  DO NOT MODIFY.\n *\n * This class was automatically generated by the\n * aapt tool from the resource data it found.  It\n * should not be modified by hand.\n */\n\npackage com.google.android.gcm.demo.app;\n\npublic final class Manifest {\n    public static final class permission {\n        /** \n     Creates a custom permission so only this app can receive its messages.\n\n     NOTE: the permission *must* be called PACKAGE.permission.C2D_MESSAGE,\n           where PACKAGE is the application's package name.\n    \n         */\n        public static final String C2D_MESSAGE=\"com.google.android.gcm.demo.app.permission.C2D_MESSAGE\";\n    }\n}\n"
  },
  {
    "path": "Android/ClientCode/gen/com/google/android/gcm/demo/app/R.java",
    "content": "/* AUTO-GENERATED FILE.  DO NOT MODIFY.\n *\n * This class was automatically generated by the\n * aapt tool from the resource data it found.  It\n * should not be modified by hand.\n */\n\npackage com.google.android.gcm.demo.app;\n\npublic final class R {\n    public static final class attr {\n    }\n    public static final class dimen {\n        /**  Default screen margins, per the Android Design guidelines. \n\n         Customize dimensions originally defined in res/values/dimens.xml (such as\n         screen margins) for sw720dp devices (e.g. 10\" tablets) in landscape here.\n    \n         */\n        public static final int activity_horizontal_margin=0x7f040000;\n        public static final int activity_vertical_margin=0x7f040001;\n    }\n    public static final class drawable {\n        public static final int ic_launcher=0x7f020000;\n        public static final int ic_stat_gcm=0x7f020001;\n    }\n    public static final class id {\n        public static final int action_settings=0x7f070004;\n        public static final int display=0x7f070000;\n        public static final int inventoryCount=0x7f070003;\n        public static final int inventoryName=0x7f070001;\n        public static final int label=0x7f070002;\n        public static final int options_clear=0x7f070007;\n        public static final int options_exit=0x7f070008;\n        public static final int options_refresh_data=0x7f070009;\n        public static final int options_register=0x7f070005;\n        public static final int options_unregister=0x7f070006;\n    }\n    public static final class layout {\n        public static final int main=0x7f030000;\n        public static final int rowlayout=0x7f030001;\n    }\n    public static final class menu {\n        public static final int inventory=0x7f060000;\n        public static final int options_menu=0x7f060001;\n    }\n    public static final class string {\n        public static final int action_settings=0x7f050013;\n        public static final int already_registered=0x7f050002;\n        public static final int app_name=0x7f050000;\n        public static final int error_config=0x7f050001;\n        public static final int gcm_deleted=0x7f050008;\n        public static final int gcm_error=0x7f050006;\n        public static final int gcm_message=0x7f050005;\n        public static final int gcm_recoverable_error=0x7f050007;\n        public static final int gcm_registered=0x7f050003;\n        public static final int gcm_unregistered=0x7f050004;\n        public static final int hello_world=0x7f050014;\n        public static final int options_clear=0x7f050010;\n        public static final int options_exit=0x7f050011;\n        public static final int options_refresh_data=0x7f050015;\n        public static final int options_register=0x7f05000e;\n        public static final int options_unregister=0x7f05000f;\n        public static final int server_register_error=0x7f05000c;\n        public static final int server_registered=0x7f05000a;\n        public static final int server_registering=0x7f050009;\n        public static final int server_unregister_error=0x7f05000d;\n        public static final int server_unregistered=0x7f05000b;\n        public static final int title_activity_inventory=0x7f050012;\n    }\n}\n"
  },
  {
    "path": "Android/ClientCode/project.properties",
    "content": "# This file is automatically generated by Android Tools.\n# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n#\n# This file must be checked in Version Control Systems.\n#\n# To customize properties used by the Ant build system edit\n# \"ant.properties\", and override values to adapt the script to your\n# project structure.\n#\n# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):\n#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt\n\n# Project target.\ntarget=android-17\n"
  },
  {
    "path": "Android/ClientCode/res/layout/main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  - Copyright 2012 Google Inc.\n  -\n  - Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n  - use this file except in compliance with the License. You may obtain a copy\n  - of the License at\n  -\n  - http://www.apache.org/licenses/LICENSE-2.0\n  -\n  - Unless required by applicable law or agreed to in writing, software\n  - distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n  - WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n  - License for the specific language governing permissions and limitations\n  - under the License.\n-->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"fill_parent\"\n    android:orientation=\"vertical\" >\n\n    <TextView\n        android:id=\"@+id/display\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\" \n        android:textIsSelectable=\"true\"/>\n\t<ListView\n        android:id=\"@android:id/list\"\n        android:layout_width=\"fill_parent\"\n        android:layout_height=\"wrap_content\" />\n</LinearLayout>"
  },
  {
    "path": "Android/ClientCode/res/layout/rowlayout.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\" >\n\n    <TextView\n        android:id=\"@+id/inventoryName\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:text=\"@+id/label\"\n        android:textSize=\"17pt\" >\n    </TextView>\n\n    <TextView\n        android:id=\"@+id/inventoryCount\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n        android:text=\"@+id/label\"\n        android:textAlignment=\"center\"\n        android:textSize=\"15pt\" >\n\n    </TextView>\n</LinearLayout> "
  },
  {
    "path": "Android/ClientCode/res/menu/inventory.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\" >\n\n    <item\n        android:id=\"@+id/action_settings\"\n        android:orderInCategory=\"100\"\n        android:showAsAction=\"never\"\n        android:title=\"@string/action_settings\"/>\n\n</menu>"
  },
  {
    "path": "Android/ClientCode/res/menu/options_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  - Copyright 2012 Google Inc.\n  -\n  - Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n  - use this file except in compliance with the License. You may obtain a copy\n  - of the License at\n  -\n  - http://www.apache.org/licenses/LICENSE-2.0\n  -\n  - Unless required by applicable law or agreed to in writing, software\n  - distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n  - WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n  - License for the specific language governing permissions and limitations\n  - under the License.\n-->\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\" >\n\n    <!-- Typically, an application registers automatically, so options below\n         are disabled. Uncomment them if you want to manually register or\n         unregister the device (you will also need to uncomment the equivalent\n         options on DemoActivity.onOptionsItemSelected())\n    -->\n    <!--\n    <item\n        android:id=\"@+id/options_register\"\n        android:title=\"@string/options_register\">\n    </item>\n    <item\n        android:id=\"@+id/options_unregister\"\n        android:title=\"@string/options_unregister\">\n    </item>\n    -->\n    <item android:id=\"@+id/options_register\" android:title=\"@string/options_register\"></item><item android:id=\"@+id/options_unregister\" android:title=\"@string/options_unregister\"></item><item\n        android:id=\"@+id/options_clear\"\n        android:title=\"@string/options_clear\">\n    </item>\n    <item\n        android:id=\"@+id/options_exit\"\n        android:title=\"@string/options_exit\">\n    </item>\n    <item android:id=\"@+id/options_refresh_data\" android:title=\"@string/options_refresh_data\"></item>\n    \n    \n\n</menu>"
  },
  {
    "path": "Android/ClientCode/res/values/dimens.xml",
    "content": "<resources>\n\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n\n</resources>"
  },
  {
    "path": "Android/ClientCode/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--\n  - Copyright 2012 Google Inc.\n  -\n  - Licensed under the Apache License, Version 2.0 (the \"License\"); you may not\n  - use this file except in compliance with the License. You may obtain a copy\n  - of the License at\n  -\n  - http://www.apache.org/licenses/LICENSE-2.0\n  -\n  - Unless required by applicable law or agreed to in writing, software\n  - distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\n  - WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\n  - License for the specific language governing permissions and limitations\n  - under the License.\n-->\n<resources>\n\n    <string name=\"app_name\">Apps Script GDL</string>\n    <string name=\"error_config\">Please set the %1$s constant and recompile the app.</string>\n    <string name=\"already_registered\">Device is already registered on server.</string>\n    <string name=\"gcm_registered\">From GCM: device successfully registered!</string>\n    <string name=\"gcm_unregistered\">From GCM: device successfully unregistered!</string>\n    <string name=\"gcm_message\">From GCM: you got message!</string>\n    <string name=\"gcm_error\">From GCM: error (%1$s).</string>\n    <string name=\"gcm_recoverable_error\">From GCM: recoverable error (%1$s).</string>\n    <string name=\"gcm_deleted\">From GCM: server deleted %1$d pending messages!</string>\n    <string name=\"server_registering\">Trying (attempt %1$d/%2$d) to register device on Demo Server.</string>\n    <string name=\"server_registered\">From Demo Server: successfully added device!</string>\n    <string name=\"server_unregistered\">From Demo Server: successfully removed device!</string>\n    <string name=\"server_register_error\">Could not register device on Demo Server after %1$d attempts.</string>\n    <string name=\"server_unregister_error\">Could not unregister device on Demo Server (%1$s).</string>\n    <string name=\"options_register\">Register</string>\n    <string name=\"options_unregister\">Unregister</string>\n    <string name=\"options_clear\">Clear</string>\n    <string name=\"options_exit\">Exit</string>\n    <string name=\"title_activity_inventory\">Inventory</string>\n    <string name=\"action_settings\">Settings</string>\n    <string name=\"hello_world\">Hello world!</string>\n    <string name=\"options_refresh_data\">Refresh Data</string>\n\n</resources>"
  },
  {
    "path": "Android/ClientCode/res/values-sw600dp/dimens.xml",
    "content": "<resources>\n\n    <!--\n         Customize dimensions originally defined in res/values/dimens.xml (such as\n         screen margins) for sw600dp devices (e.g. 7\" tablets) here.\n    -->\n\n</resources>"
  },
  {
    "path": "Android/ClientCode/res/values-sw720dp-land/dimens.xml",
    "content": "<resources>\n\n    <!--\n         Customize dimensions originally defined in res/values/dimens.xml (such as\n         screen margins) for sw720dp devices (e.g. 10\" tablets) in landscape here.\n    -->\n    <dimen name=\"activity_horizontal_margin\">128dp</dimen>\n\n</resources>"
  },
  {
    "path": "Android/ClientCode/src/com/google/android/gcm/demo/app/CommonUtilities.java",
    "content": "/*\n * Copyright 2012 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.gcm.demo.app;\n\nimport android.content.Context;\nimport android.content.Intent;\n\n/**\n * Helper class providing methods and constants common to other classes in the\n * app.\n */\npublic final class CommonUtilities {\n\n\t/**\n\t * Base URL of the Demo Server (such as http://my_host:8080/gcm-demo)\n\t */\n\tstatic final String SERVER_URL = \"https://script.google.com/macros/s/YOUR_SERVER_URL/exec\";\n\n\t/**\n\t * Google API project id registered to use GCM.\n\t */\n\tstatic final String SENDER_ID = \"YOUR_SENDER_ID\";\n\n\t/**\n\t * Tag used on log messages.\n\t */\n\tstatic final String TAG = \"GCMDemo\";\n\n\t/**\n\t * Intent used to display a message in the screen.\n\t */\n\tstatic final String DISPLAY_MESSAGE_ACTION =\n\t\t\t\"com.google.android.gcm.demo.app.DISPLAY_MESSAGE\";\n\n\t/**\n\t * Intent's extra that contains the message to be displayed.\n\t */\n\tstatic final String EXTRA_MESSAGE = \"message\";\n\n\t/**\n\t * Notifies UI to display a message.\n\t * <p>\n\t * This method is defined in the common helper because it's used both by\n\t * the UI and the background service.\n\t *\n\t * @param context application's context.\n\t * @param message message to be displayed.\n\t */\n\tstatic void displayMessage(Context context, String message) {\n\t\tIntent intent = new Intent(DISPLAY_MESSAGE_ACTION);\n\t\tintent.putExtra(EXTRA_MESSAGE, message);\n\t\tcontext.sendBroadcast(intent);\n\t}\n}\n"
  },
  {
    "path": "Android/ClientCode/src/com/google/android/gcm/demo/app/DemoActivity.java",
    "content": "/*\n * Copyright 2012 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.gcm.demo.app;\n\nimport static com.google.android.gcm.demo.app.CommonUtilities.DISPLAY_MESSAGE_ACTION;\nimport static com.google.android.gcm.demo.app.CommonUtilities.EXTRA_MESSAGE;\nimport static com.google.android.gcm.demo.app.CommonUtilities.SENDER_ID;\nimport static com.google.android.gcm.demo.app.CommonUtilities.SERVER_URL;\n\nimport java.util.ArrayList;\n\nimport org.apache.http.HttpResponse;\nimport org.apache.http.HttpStatus;\nimport org.apache.http.client.HttpClient;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.impl.client.DefaultHttpClient;\nimport org.apache.http.util.EntityUtils;\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport com.google.android.gcm.GCMRegistrar;\n\nimport android.app.ListActivity;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.os.AsyncTask;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.Menu;\nimport android.view.MenuInflater;\nimport android.view.MenuItem;\nimport android.widget.TextView;\n\n/**\n * Main UI for the demo app.\n */\npublic class DemoActivity extends ListActivity {\n\n\tTextView mDisplay;\n\tAsyncTask<Void, Void, Void> mRegisterTask;\n\n\t@Override\n\tpublic void onCreate(Bundle savedInstanceState) {\n\t\tsuper.onCreate(savedInstanceState);\n\t\tcheckNotNull(SERVER_URL, \"SERVER_URL\");\n\t\tcheckNotNull(SENDER_ID, \"SENDER_ID\");\n\t\t// Make sure the device has the proper dependencies.\n\t\tGCMRegistrar.checkDevice(this);\n\t\t// Make sure the manifest was properly set - comment out this line\n\t\t// while developing the app, then uncomment it when it's ready.\n\t\tGCMRegistrar.checkManifest(this);\n\t\tsetContentView(R.layout.main);\n\t\tmDisplay = (TextView) findViewById(R.id.display);\n\t\tregisterReceiver(mHandleMessageReceiver,\n\t\t\t\tnew IntentFilter(DISPLAY_MESSAGE_ACTION));\n\t\tfinal String regId = GCMRegistrar.getRegistrationId(this);\n\t\tfinal Context context = this;\n\n\t\tif (regId.equals(\"\")) {\n\t\t\t// Automatically registers application on startup.\n\t\t\tGCMRegistrar.register(this, SENDER_ID);\n\t\t} else {\n\t\t\t// Device is already registered on GCM, check server.\n\t\t\tif (GCMRegistrar.isRegisteredOnServer(this)) {\n\t\t\t\t// Skips registration.\n\t\t\t\tmDisplay.append(getString(R.string.already_registered) + \"\\n\");\n\t\t\t} else {\n\t\t\t\t// Try to register again, but not in the UI thread.\n\t\t\t\t// It's also necessary to cancel the thread onDestroy(),\n\t\t\t\t// hence the use of AsyncTask instead of a raw thread.\n\t\t\t\tmRegisterTask = new AsyncTask<Void, Void, Void>() {\n\n\t\t\t\t\t@Override\n\t\t\t\t\tprotected Void doInBackground(Void... params) {\n\t\t\t\t\t\tboolean registered =\n\t\t\t\t\t\t\t\tServerUtilities.register(context, regId);\n\t\t\t\t\t\t// At this point all attempts to register with the app\n\t\t\t\t\t\t// server failed, so we need to unregister the device\n\t\t\t\t\t\t// from GCM - the app will try to register again when\n\t\t\t\t\t\t// it is restarted. Note that GCM will send an\n\t\t\t\t\t\t// unregistered callback upon completion, but\n\t\t\t\t\t\t// GCMIntentService.onUnregistered() will ignore it.\n\t\t\t\t\t\tif (!registered) {\n\t\t\t\t\t\t\tGCMRegistrar.unregister(context);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\t@Override\n\t\t\t\t\tprotected void onPostExecute(Void result) {\n\t\t\t\t\t\tmRegisterTask = null;\n\t\t\t\t\t}\n\n\t\t\t\t};\n\t\t\t\tmRegisterTask.execute(null, null, null);\n\t\t\t}\n\t\t}\n\n\t\tMyTask task = new MyTask();\n\t\ttask.execute();\n\n\t}\n\n\tprivate class MyTask extends AsyncTask<Void, Void, Void> {\n\t\tArrayList<Item> items = new ArrayList<Item>();\n\t\t@Override\n\t\tprotected Void doInBackground(Void... params) {\n\t\t\ttry {\n\t\t\t\tHttpClient hc = new DefaultHttpClient();\n\t\t\t\tString URL = \"https://script.google.com/macros/s/YOU_DATA_URL/exec\";                   \n\t\t\t\tHttpGet get = new HttpGet(URL);\n\t\t\t\tHttpResponse rp = hc.execute(get);\n\t\t\t\tif(rp.getStatusLine().getStatusCode() == HttpStatus.SC_OK)\n\t\t\t\t{\n\t\t\t\t\tString result = EntityUtils.toString(rp.getEntity());\n\t\t\t\t\tJSONArray objects = new JSONArray(result);\n\n\t\t\t\t\tfor (int i = 0; i < objects.length(); i++) {\n\t\t\t\t\t\tJSONObject session = objects.getJSONObject(i);\n\t\t\t\t\t\tItem item = new Item();\n\t\t\t\t\t\titem.name = session.getString(\"item\");\n\t\t\t\t\t\titem.count = session.getString(\"quantity\");\n\t\t\t\t\t\titems.add(item);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\tLog.e(\"ItemFeed\", \"Error loading JSON\", e);\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\n\t\t@Override\n\t\tprotected void onPostExecute(Void result) {\n\t\t\tSimpleArrayAdapter adapter = new SimpleArrayAdapter(DemoActivity.this,items);\n\t\t\tsetListAdapter(adapter);\n\t\t}\n\t}\n\n\t@Override\n\tpublic boolean onCreateOptionsMenu(Menu menu) {\n\t\tMenuInflater inflater = getMenuInflater();\n\t\tinflater.inflate(R.menu.options_menu, menu);\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic boolean onOptionsItemSelected(MenuItem item) {\n\t\tswitch(item.getItemId()) {\n\t\t/*\n\t\t * Typically, an application registers automatically, so options\n\t\t * below are disabled. Uncomment them if you want to manually\n\t\t * register or unregister the device (you will also need to\n\t\t * uncomment the equivalent options on options_menu.xml).\n\t\t */\n\n\t\tcase R.id.options_register:\n\t\t\tGCMRegistrar.register(this, SENDER_ID);\n\t\t\treturn true;\n\t\tcase R.id.options_unregister:\n\t\t\tGCMRegistrar.unregister(this);\n\t\t\treturn true;\n\n\t\tcase R.id.options_clear:\n\t\t\tmDisplay.setText(null);\n\t\t\treturn true;\n\t\tcase R.id.options_exit:\n\t\t\tfinish();\n\t\t\treturn true;\n\t\tcase R.id.options_refresh_data:\n\t\t\tMyTask task = new MyTask();\n\t\t\ttask.execute();\n\t\t\treturn true;\n\t\tdefault:\n\t\t\treturn super.onOptionsItemSelected(item);\n\t\t}\n\t}\n\n\t@Override\n\tprotected void onDestroy() {\n\t\tif (mRegisterTask != null) {\n\t\t\tmRegisterTask.cancel(true);\n\t\t}\n\t\tunregisterReceiver(mHandleMessageReceiver);\n\t\tGCMRegistrar.onDestroy(this);\n\t\tsuper.onDestroy();\n\t}\n\n\tprivate void checkNotNull(Object reference, String name) {\n\t\tif (reference == null) {\n\t\t\tthrow new NullPointerException(\n\t\t\t\t\tgetString(R.string.error_config, name));\n\t\t}\n\t}\n\n\tprivate final BroadcastReceiver mHandleMessageReceiver =\n\t\t\tnew BroadcastReceiver() {\n\t\t@Override\n\t\tpublic void onReceive(Context context, Intent intent) {\n\t\t\tString newMessage = intent.getExtras().getString(EXTRA_MESSAGE);\n\t\t\tmDisplay.append(newMessage + \"\\n\");\n\n\t\t\tMyTask task = new MyTask();\n\t\t\ttask.execute();\n\t\t}\n\t};\n\n}"
  },
  {
    "path": "Android/ClientCode/src/com/google/android/gcm/demo/app/GCMIntentService.java",
    "content": "/*\n * Copyright 2012 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.gcm.demo.app;\n\nimport static com.google.android.gcm.demo.app.CommonUtilities.SENDER_ID;\nimport static com.google.android.gcm.demo.app.CommonUtilities.displayMessage;\n\nimport android.annotation.TargetApi;\nimport android.app.Notification;\nimport android.app.NotificationManager;\nimport android.app.PendingIntent;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.os.Build;\nimport android.util.Log;\n\nimport com.google.android.gcm.GCMBaseIntentService;\nimport com.google.android.gcm.GCMRegistrar;\n\n/**\n * IntentService responsible for handling GCM messages.\n */\npublic class GCMIntentService extends GCMBaseIntentService {\n\n    @SuppressWarnings(\"hiding\")\n    private static final String TAG = \"GCMIntentService\";\n\n    public GCMIntentService() {\n        super(SENDER_ID);\n    }\n\n    @Override\n    protected void onRegistered(Context context, String registrationId) {\n        Log.i(TAG, \"Device registered: regId = \" + registrationId);\n        displayMessage(context, getString(R.string.gcm_registered));\n        ServerUtilities.register(context, registrationId);\n    }\n\n    @Override\n    protected void onUnregistered(Context context, String registrationId) {\n        Log.i(TAG, \"Device unregistered\");\n        displayMessage(context, getString(R.string.gcm_unregistered));\n        if (GCMRegistrar.isRegisteredOnServer(context)) {\n            ServerUtilities.unregister(context, registrationId);\n        } else {\n            // This callback results from the call to unregister made on\n            // ServerUtilities when the registration to the server failed.\n            Log.i(TAG, \"Ignoring unregister callback\");\n        }\n    }\n\n    @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)\n\t@Override\n    protected void onMessage(Context context, Intent intent) {\n    \tLog.i(TAG, \"Received message\");\n        String message = intent.getExtras().getString(\"message\", getString(R.string.gcm_message));\n        displayMessage(context, message);\n        // notifies user\n        generateNotification(context, message);\n    }\n\n    @Override\n    protected void onDeletedMessages(Context context, int total) {\n        Log.i(TAG, \"Received deleted messages notification\");\n        String message = getString(R.string.gcm_deleted, total);\n        displayMessage(context, message);\n        // notifies user\n        generateNotification(context, message);\n    }\n\n    @Override\n    public void onError(Context context, String errorId) {\n        Log.i(TAG, \"Received error: \" + errorId);\n        displayMessage(context, getString(R.string.gcm_error, errorId));\n    }\n\n    @Override\n    protected boolean onRecoverableError(Context context, String errorId) {\n        // log message\n        Log.i(TAG, \"Received recoverable error: \" + errorId);\n        displayMessage(context, getString(R.string.gcm_recoverable_error,\n                errorId));\n        return super.onRecoverableError(context, errorId);\n    }\n\n    /**\n     * Issues a notification to inform the user that server has sent a message.\n     */\n    private static void generateNotification(Context context, String message) {\n        int icon = R.drawable.ic_stat_gcm;\n        long when = System.currentTimeMillis();\n        NotificationManager notificationManager = (NotificationManager)\n                context.getSystemService(Context.NOTIFICATION_SERVICE);\n        Notification notification = new Notification(icon, message, when);\n        String title = context.getString(R.string.app_name);\n        Intent notificationIntent = new Intent(context, DemoActivity.class);\n        // set intent so it does not start a new activity\n        notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |\n                Intent.FLAG_ACTIVITY_SINGLE_TOP);\n        PendingIntent intent =\n                PendingIntent.getActivity(context, 0, notificationIntent, 0);\n        notification.setLatestEventInfo(context, title, message, intent);\n        notification.flags |= Notification.FLAG_AUTO_CANCEL;\n        notification.defaults = Notification.DEFAULT_ALL;\n        notificationManager.notify(0, notification);\n    }\n\n}\n"
  },
  {
    "path": "Android/ClientCode/src/com/google/android/gcm/demo/app/Item.java",
    "content": "package com.google.android.gcm.demo.app;\n\npublic class Item {\n\tString name;\n    String count;\n}\n"
  },
  {
    "path": "Android/ClientCode/src/com/google/android/gcm/demo/app/ServerUtilities.java",
    "content": "/*\n * Copyright 2012 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *   http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\npackage com.google.android.gcm.demo.app;\n\nimport static com.google.android.gcm.demo.app.CommonUtilities.SERVER_URL;\nimport static com.google.android.gcm.demo.app.CommonUtilities.TAG;\nimport static com.google.android.gcm.demo.app.CommonUtilities.displayMessage;\n\nimport com.google.android.gcm.GCMRegistrar;\n\nimport android.content.Context;\nimport android.util.Log;\n\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Map.Entry;\nimport java.util.Random;\n\n/**\n * Helper class used to communicate with the demo server.\n */\npublic final class ServerUtilities {\n\n\tprivate static final int MAX_ATTEMPTS = 5;\n\tprivate static final int BACKOFF_MILLI_SECONDS = 2000;\n\tprivate static final Random random = new Random();\n\n\t/**\n\t * Register this account/device pair within the server.\n\t *\n\t * @return whether the registration succeeded or not.\n\t */\n\tstatic boolean register(final Context context, final String regId) {\n\t\tLog.i(TAG, \"registering device (regId = \" + regId + \")\");\n\t\tString serverUrl = SERVER_URL;// + \"/register\";\n\t\tMap<String, String> params = new HashMap<String, String>();\n\t\tparams.put(\"regId\", regId);\n\t\tparams.put(\"type\", \"register\");\n\t\tlong backoff = BACKOFF_MILLI_SECONDS + random.nextInt(1000);\n\t\t// Once GCM returns a registration id, we need to register it in the\n\t\t// demo server. As the server might be down, we will retry it a couple\n\t\t// times.\n\t\tfor (int i = 1; i <= MAX_ATTEMPTS; i++) {\n\t\t\tLog.d(TAG, \"Attempt #\" + i + \" to register\");\n\t\t\ttry {\n\t\t\t\tdisplayMessage(context, context.getString(\n\t\t\t\t\t\tR.string.server_registering, i, MAX_ATTEMPTS));\n\t\t\t\tpost(serverUrl, params);\n\t\t\t\tGCMRegistrar.setRegisteredOnServer(context, true);\n\t\t\t\tString message = context.getString(R.string.server_registered);\n\t\t\t\tCommonUtilities.displayMessage(context, message);\n\t\t\t\treturn true;\n\t\t\t} catch (IOException e) {\n\t\t\t\t// Here we are simplifying and retrying on any error; in a real\n\t\t\t\t// application, it should retry only on unrecoverable errors\n\t\t\t\t// (like HTTP error code 503).\n\t\t\t\tLog.e(TAG, \"Failed to register on attempt \" + i, e);\n\t\t\t\tif (i == MAX_ATTEMPTS) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tLog.d(TAG, \"Sleeping for \" + backoff + \" ms before retry\");\n\t\t\t\t\tThread.sleep(backoff);\n\t\t\t\t} catch (InterruptedException e1) {\n\t\t\t\t\t// Activity finished before we complete - exit.\n\t\t\t\t\tLog.d(TAG, \"Thread interrupted: abort remaining retries!\");\n\t\t\t\t\tThread.currentThread().interrupt();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\t// increase backoff exponentially\n\t\t\t\tbackoff *= 2;\n\t\t\t}\n\t\t}\n\t\tString message = context.getString(R.string.server_register_error,\n\t\t\t\tMAX_ATTEMPTS);\n\t\tCommonUtilities.displayMessage(context, message);\n\t\treturn false;\n\t}\n\n\t/**\n\t * Unregister this account/device pair within the server.\n\t */\n\tstatic void unregister(final Context context, final String regId) {\n\t\tLog.i(TAG, \"unregistering device (regId = \" + regId + \")\");\n\t\tString serverUrl = SERVER_URL;// + \"/unregister\";\n\t\tMap<String, String> params = new HashMap<String, String>();\n\t\tparams.put(\"regId\", regId);\n\t\tparams.put(\"type\",\"unregister\");\n\t\ttry {\n\t\t\tpost(serverUrl, params);\n\t\t\tGCMRegistrar.setRegisteredOnServer(context, false);\n\t\t\tString message = context.getString(R.string.server_unregistered);\n\t\t\tCommonUtilities.displayMessage(context, message);\n\t\t} catch (IOException e) {\n\t\t\t// At this point the device is unregistered from GCM, but still\n\t\t\t// registered in the server.\n\t\t\t// We could try to unregister again, but it is not necessary:\n\t\t\t// if the server tries to send a message to the device, it will get\n\t\t\t// a \"NotRegistered\" error message and should unregister the device.\n\t\t\tString message = context.getString(R.string.server_unregister_error,\n\t\t\t\t\te.getMessage());\n\t\t\tCommonUtilities.displayMessage(context, message);\n\t\t}\n\t}\n\n\t/**\n\t * Issue a POST request to the server.\n\t *\n\t * @param endpoint POST address.\n\t * @param params request parameters.\n\t *\n\t * @throws IOException propagated from POST.\n\t */\n\tprivate static void post(String endpoint, Map<String, String> params)\n\t\t\tthrows IOException {\n\t\tURL url;\n\t\ttry {\n\t\t\turl = new URL(endpoint);\n\t\t} catch (MalformedURLException e) {\n\t\t\tthrow new IllegalArgumentException(\"invalid url: \" + endpoint);\n\t\t}\n\t\tStringBuilder bodyBuilder = new StringBuilder();\n\t\tIterator<Entry<String, String>> iterator = params.entrySet().iterator();\n\t\t// constructs the POST body using the parameters\n\t\twhile (iterator.hasNext()) {\n\t\t\tEntry<String, String> param = iterator.next();\n\t\t\tbodyBuilder.append(param.getKey()).append('=')\n\t\t\t.append(param.getValue());\n\t\t\tif (iterator.hasNext()) {\n\t\t\t\tbodyBuilder.append('&');\n\t\t\t}\n\t\t}\n\t\tString body = bodyBuilder.toString();\n\t\tLog.v(TAG, \"Posting '\" + body + \"' to \" + url);\n\t\tbyte[] bytes = body.getBytes();\n\t\tHttpURLConnection conn = null;\n\t\ttry {\n\t\t\tconn = (HttpURLConnection) url.openConnection();\n\t\t\tconn.setDoOutput(true);\n\t\t\tconn.setUseCaches(false);\n\t\t\tconn.setFixedLengthStreamingMode(bytes.length);\n\t\t\tconn.setRequestMethod(\"POST\");\n\t\t\tconn.setRequestProperty(\"Content-Type\",\n\t\t\t\t\t\"application/x-www-form-urlencoded;charset=UTF-8\");\n\t\t\t// post the request\n\t\t\tOutputStream out = conn.getOutputStream();\n\t\t\tout.write(bytes);\n\t\t\tout.close();\n\t\t\t// handle the response\n\t\t\tint status = conn.getResponseCode();\n\t\t\tif (status != 200) {\n\t\t\t\tthrow new IOException(\"Post failed with error code \" + status);\n\t\t\t}\n\t\t} finally {\n\t\t\tif (conn != null) {\n\t\t\t\tconn.disconnect();\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "Android/ClientCode/src/com/google/android/gcm/demo/app/SimpleArrayAdapter.java",
    "content": "package com.google.android.gcm.demo.app;\n\nimport java.util.ArrayList;\n\nimport android.content.Context;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\nimport android.widget.TextView;\n\npublic class SimpleArrayAdapter extends ArrayAdapter<Item> {\n\tprivate final Context context;\n\tprivate final ArrayList<Item> values;\n\n\tpublic SimpleArrayAdapter(Context context, ArrayList<Item> values) {\n\t\tsuper(context, R.layout.rowlayout, values);\n\t\tthis.context = context;\n\t\tthis.values = values;\n\t}\n\n\t@Override\n\tpublic View getView(int position, View convertView, ViewGroup parent) {\n\t\tLayoutInflater inflater = (LayoutInflater) context\n\t\t\t\t.getSystemService(Context.LAYOUT_INFLATER_SERVICE);\n\t\tView rowView = inflater.inflate(R.layout.rowlayout, parent, false);\n\t\tItem i = values.get(position);\n\n\n\t\tTextView nameTextView = (TextView) rowView.findViewById(R.id.inventoryName);\n\t\tTextView countTextView = (TextView) rowView.findViewById(R.id.inventoryCount);\n\t\tnameTextView.setText(i.name);\n\n\t\tcountTextView.setText(i.count);\n\n\t\treturn rowView;\n\t}\n} "
  },
  {
    "path": "Android/GCM_Registrar_Script/code.gs",
    "content": "function doGet() {\n  return HtmlService.createHtmlOutputFromFile('ui');\n}\n\n\n//import shared db library\n//used the shared version intead of - \n//ScriptDb.getMyDb()\nvar db = SharedDb.getDb();\n\nfunction doPost(e) {\n  if(e.parameter.regId){\n    var reg = db.query({regId : e.parameter.regId}).next();\n    if(reg && e.parameter.type === 'unregister'){\n      db.remove(reg);\n    }\n    else if(!reg && e.parameter.type === 'register'){\n      db.save(e.parameter);\n    }\n  }\n}\n"
  },
  {
    "path": "Android/GCM_Registrar_Script/scriptdb.gs",
    "content": "var db = SharedDb.getDb();\n\n\nfunction saveObjToDb(obj){\n  try{\n    var savedObj = db.save(obj);\n    return \"Saved ID - \" + savedObj.getId();\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction getCount(query){\n  try{\n    return \"Current count for query is - \" + db.count(query);\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction loadIDsFromDb(idlist){\n  try{\n    return db.load(idlist);\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction deleteByIds(idlist){\n  var obs_to_remove = loadIDsFromDb(idlist);\n  var results = db.removeBatch(obs_to_remove, false);\n  if (db.allOk(results)) {\n    return \"Delete by IDs successfull!\";\n  }\n  var failedObs = [];\n  for (var i = 0; i < results.length; i++) {\n    if (!results[i].successful()) {\n      failedObs.push(obs_to_remove[i]);\n    }\n  }\n  return \"Failed to delete \" + failedObs.length + \" item(s) out of \" + results.length;\n}\n\nfunction queryFromDb(query){\n  try{\n    var result = db.query(query);\n    var response = {};\n    while (result.hasNext()) {\n      var current = result.next();\n      response[current.getId()] = current;\n    }\n    return response;\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction deleteAll(query) {\n  try{\n    while (true) {\n      var result = db.query(query); \n      if (result.getSize() == 0) {\n        break;\n      }\n      while (result.hasNext()) {\n        db.remove(result.next());\n      }\n    }\n    return \"Delete for specified query successful!\";\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction log(msg){\n  //write to a logger here\n  //DocsList.getFileById('0B0JNj_IM2wiPMS1lZTFhZjVjNC0yZTBjLTRiOWItYWVhMy0yYTU1ZjdkMGVkMGE').append(msg+'\\n');\n}"
  },
  {
    "path": "Android/GCM_Registrar_Script/ui.html",
    "content": "<html>\n  <style>\n    pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; }\n    .string { color: green; }\n    .number { color: darkorange; }\n    .boolean { color: blue; }\n    .null { color: magenta; }\n    .key { color: red; }\n  </style>\n  <script src=\"http://code.jquery.com/jquery-1.7.2.min.js\"></script>\n  <script>\n    function output(inp) {\n      $('#results').prepend(inp);\n    }\n    //courtesy of http://stackoverflow.com/a/7220510/662551\n    function syntaxHighlight(json) {\n      json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n      return json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) {\n        var cls = 'number';\n        if (/^\"/.test(match)) {\n          if (/:$/.test(match)) {\n            cls = 'key';\n          } else {\n            cls = 'string';\n          }\n        } else if (/true|false/.test(match)) {\n          cls = 'boolean';\n        } else if (/null/.test(match)) {\n          cls = 'null';\n        }\n        return '<span class=\"' + cls + '\">' + match + '</span>';\n      });\n    }\n    \n    $(document).ready(function() {\n      $('#saveItem').click(function(){\n        var input = $('#textarea').val();\n        input = JSON.parse(input);\n        google.script.run.withSuccessHandler(handleMessageResponse).withFailureHandler(addToLog).saveObjToDb(input);\n      });\n      $('#runQuery').click(function(){\n        var input = $('#textarea').val();\n        input = JSON.parse(input);\n        google.script.run.withSuccessHandler(handleQueryResponse).withFailureHandler(addToLog).queryFromDb(input);\n      });\n      $('#deleteAll').click(function(){\n        var input = $('#textarea').val();\n        input = JSON.parse(input);\n        google.script.run.withSuccessHandler(handleMessageResponse).withFailureHandler(addToLog).deleteAll(input);\n      });\n      $('#loadId').click(function(){\n        var input = $('#textarea').val();\n        var re = /\\s*,\\s*/;\n        var idList = input.split(re);\n        google.script.run.withSuccessHandler(handleQueryResponse).withFailureHandler(addToLog).loadIDsFromDb(idList);\n      });\n      $('#deleteByIds').click(function(){\n        var input = $('#textarea').val();\n        var re = /\\s*,\\s*/;\n        var idList = input.split(re);\n        google.script.run.withSuccessHandler(handleMessageResponse).withFailureHandler(addToLog).deleteByIds(idList);\n      });\n      $('#count').click(function(){\n        var input = $('#textarea').val();\n        input = JSON.parse(input);\n        google.script.run.withSuccessHandler(handleMessageResponse).withFailureHandler(addToLog).getCount(input);\n      });\n    });\n    function handleQueryResponse(responseObj){\n      $('#results').prepend('<hr><hr>');\n      if(Object.keys(responseObj).length==0){\n        output(\"No results found for query!\");\n      }\n      //TODO: for load by IDs, this will print index, not object IDs\n      for (var prop in responseObj) {\n        if (responseObj.hasOwnProperty(prop)) {\n          output('<pre>ID: ' + prop+'\\n'+syntaxHighlight(JSON.stringify(responseObj[prop], undefined, 4))+'</pre>');\n        }\n      }\n    }\n    function handleMessageResponse(response){\n      $('#results').prepend('<hr><hr>');\n      output(response); \n    }\n    function addToLog(error){\n      $('#results').prepend('<hr><hr>');\n      output(\"Error \" + JSON.stringify(error)); \n    }\n  </script>\n  <body>\n    <h2>Simple ScriptDb tester</h2>\n    \n    <b>Enter a JavaScript Object to store, a ScriptDb query to run or IDs to lookup and press the appropriate button below</b><br>\n    <textarea id=\"textarea\" rows=\"10\" cols=\"100\"></textarea><br>\n    <button id=\"count\">Get Count</button>\n    <button id=\"saveItem\">Save Object</button>\n    <button id=\"runQuery\">Run Query</button>\n    <button id=\"loadId\">Load by ID</button>\n    <button id=\"deleteByIds\">Delete by ID</button>\n    <button id=\"deleteAll\">Delete for Query</button>\n    \n    <br><br>\n    <div id=\"results\"></div>\n    <br><br>\n    By <a href=\"https://plus.google.com/117678608428606781684/posts\">Arun Nagarajan</a>\n  </body>\n</html>"
  },
  {
    "path": "Android/InventoryContentService.gs",
    "content": "function doGet() {\n  var ss = SpreadsheetApp.openById('YOUR_SS_ID');\n  var sheet = ss.getSheets()[0];\n\n  // Get the range of cells that store employee data.\n  var employeeDataRange = ss.getRangeByName(\"inventoryData\");\n\n  // For every row of employee data, generate an employee object.\n  var employeeObjects = getRowsData(sheet, employeeDataRange);\n  return ContentService.createTextOutput(JSON.stringify(employeeObjects)).setMimeType(ContentService.MimeType.JSON);\n}\n\n\n// getRowsData iterates row by row in the input range and returns an array of objects.\n// Each object contains all the data for a given row, indexed by its normalized column name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - columnHeadersRowIndex: specifies the row number where the column names are stored.\n//       This argument is optional and it defaults to the row immediately above range; \n// Returns an Array of objects.\nfunction getRowsData(sheet, range, columnHeadersRowIndex) {\n  columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;\n  var numColumns = range.getLastColumn() - range.getColumn() + 1;\n  var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);\n  var headers = headersRange.getValues()[0];\n  return getObjects(range.getValues(), normalizeHeaders(headers));\n}\n\n\n// For every row of data in data, generates an object that contains the data. Names of\n// object fields are defined in keys.\n// Arguments:\n//   - data: JavaScript 2d array\n//   - keys: Array of Strings that define the property names for the objects to create\nfunction getObjects(data, keys) {\n  var objects = [];\n  for (var i = 0; i < data.length; ++i) {\n    var object = {};\n    var hasData = false;\n    for (var j = 0; j < data[i].length; ++j) {\n      var cellData = data[i][j];\n      if (isCellEmpty(cellData)) {\n        continue;\n      }\n      object[keys[j]] = cellData;\n      hasData = true;\n    }\n    if (hasData) {\n      objects.push(object);\n    }\n  }\n  return objects;\n}\n\n// Returns an Array of normalized Strings.\n// Arguments:\n//   - headers: Array of Strings to normalize\nfunction normalizeHeaders(headers) {\n  var keys = [];\n  for (var i = 0; i < headers.length; ++i) {\n    var key = normalizeHeader(headers[i]);\n    if (key.length > 0) {\n      keys.push(key);\n    }\n  }\n  return keys;\n}\n\n// Normalizes a string, by removing all alphanumeric characters and using mixed case\n// to separate words. The output will always start with a lower case letter.\n// This function is designed to produce JavaScript object property names.\n// Arguments:\n//   - header: string to normalize\n// Examples:\n//   \"First Name\" -> \"firstName\"\n//   \"Market Cap (millions) -> \"marketCapMillions\n//   \"1 number at the beginning is ignored\" -> \"numberAtTheBeginningIsIgnored\"\nfunction normalizeHeader(header) {\n  var key = \"\";\n  var upperCase = false;\n  for (var i = 0; i < header.length; ++i) {\n    var letter = header[i];\n    if (letter == \" \" && key.length > 0) {\n      upperCase = true;\n      continue;\n    }\n    if (!isAlnum(letter)) {\n      continue;\n    }\n    if (key.length == 0 && isDigit(letter)) {\n      continue; // first character must be a letter\n    }\n    if (upperCase) {\n      upperCase = false;\n      key += letter.toUpperCase();\n    } else {\n      key += letter.toLowerCase();\n    }\n  }\n  return key;\n}\n\n// Returns true if the cell where cellData was read from is empty.\n// Arguments:\n//   - cellData: string\nfunction isCellEmpty(cellData) {\n  return typeof(cellData) == \"string\" && cellData == \"\";\n}\n\n// Returns true if the character char is alphabetical, false otherwise.\nfunction isAlnum(char) {\n  return char >= 'A' && char <= 'Z' ||\n    char >= 'a' && char <= 'z' ||\n    isDigit(char);\n}\n\n// Returns true if the character char is a digit, false otherwise.\nfunction isDigit(char) {\n  return char >= '0' && char <= '9';\n}\n\n// Given a JavaScript 2d Array, this function returns the transposed table.\n// Arguments:\n//   - data: JavaScript 2d Array\n// Returns a JavaScript 2d Array\n// Example: arrayTranspose([[1,2,3],[4,5,6]]) returns [[1,4],[2,5],[3,6]].\nfunction arrayTranspose(data) {\n  if (data.length == 0 || data[0].length == 0) {\n    return null;\n  }\n\n  var ret = [];\n  for (var i = 0; i < data[0].length; ++i) {\n    ret.push([]);\n  }\n\n  for (var i = 0; i < data.length; ++i) {\n    for (var j = 0; j < data[i].length; ++j) {\n      ret[j][i] = data[i][j];\n    }\n  }\n\n  return ret;\n}\n"
  },
  {
    "path": "Android/SharedDb.gs",
    "content": "var db = ScriptDb.getMyDb();\n\nfunction getDb(){\n  return db;\n}\n\nfunction sendGCM(msg){\n  msg = msg ||  'hello world!'; //give default message for debugging\n  \n  var regIds  = [];\n  var result = db.query({});\n  while (result.hasNext()) {\n    var current = result.next();\n    regIds.push(current.regId);\n  }\n  \n  var apiKey = 'YOUR_API_KEY';\n  \n  var payload = {'registration_ids' : regIds,\n                 'data' : {\n                   'message' : msg\n                 }};\n  var urlFetchOptions =  {'contentType' : 'application/json',\n                          'headers' : {'Authorization' : 'key=' + apiKey},\n                          'method' : 'post',\n                          'payload' : JSON.stringify(payload)};\n  \n  var gcmUrl = 'https://android.googleapis.com/gcm/send';\n  var response = UrlFetchApp.fetch(gcmUrl,urlFetchOptions).getContentText()\n  \n  Logger.log(response);//for testing purposes. improve error handling here\n}"
  },
  {
    "path": "Android/readme.md",
    "content": "Click the image below for video with more details - \n\n[![ScreenShot](https://i.ytimg.com/vi/RSgMEtRl0sw/hqdefault.jpg)](http://www.youtube.com/watch?v=RSgMEtRl0sw&list=PL68F511F6E3C122EB)"
  },
  {
    "path": "Android/track-and-notify.gs",
    "content": "//create an onEdit trigger for this! in a spreadsheet bound script\n//import shared db library\nfunction sendNotification(){\n  var msg = \"Edited at: \" + new Date().toTimeString();  \n  SharedDb.sendGCM(msg);\n}\n"
  },
  {
    "path": "ContentService/Addressbook-VCF/Code.gs",
    "content": "function doGet() {\n  var contact = ContactsApp.getContact('youremail@gmail.com');  \n  \n  var t = HtmlService.createTemplateFromFile('vcard_template');\n  t.contact = contact;\n  var output = t.evaluate().getContent();\n  \n  return ContentService.createTextOutput(output).downloadAsFile('ContactCard.vcf');\n}\n"
  },
  {
    "path": "ContentService/Addressbook-VCF/vcard_template.html",
    "content": "BEGIN:VCARD\nFN:<?= contact.getFullName(); ?>\nTITLE:General Manager\nORG:<?= contact.getFullName(); ?>;North American Division;Manufacturing\nADR;POSTAL;WORK:;;P.O. Box 10010;AnyCity;AnyState;00000;U.S.A.\nLABEL;POSTAL;WORK;ENCODING=QUOTED-PRINTABLE:P.O. Box 10010=0D=0A=\nAnywhere, TN  37849=0D=0A=U.S.A.\nADR;PARCEL;WORK:;133 Anywhere St.;Suite 360;AnyCity;AnyState;00000;U.S.A.\nLABEL;POSTAL;WORK;ENCODING=QUOTED-PRINTABLE:133 Anywhere St.=0D=0A=\nAnywhere, TN  37849=0D=0A=U.S.A.\nTEL;Work;VOICE;MESG;PREF:+1-234-456-7891 x56473\nTEL;Home:+1-234-456-7891\nTEL;Pager:+1-234-456-7891\nTEL;Cell:+1-234-456-7891\nTEL;Modem;FAX:+1-234-456-7891,,*3\nEMAIL;Internet:anywhere@anywhere.com\nURL:http://www.anywhere.com/mrh.vcf\nUID:http://www.anywhere.com/mrh.vcf\nTZ:-0500\nBDAY:1997-11-29\nREV:20090401T065518\nVERSION:2.1\nEND:VCARD"
  },
  {
    "path": "ContentService/Gmail-RSS/code.gs",
    "content": "function doGet() {  \n  var content = HtmlService.createTemplateFromFile('rss').evaluate().getContent();\n  return ContentService.createTextOutput(content).setMimeType(ContentService.MimeType.RSS);\n}\n"
  },
  {
    "path": "ContentService/Gmail-RSS/rss.html",
    "content": "<rss version = \"2.0\">\n<channel>\n  <title>Gmail Feed</title>\n  <? var threads = GmailApp.getInboxThreads();\n     var messages = GmailApp.getMessagesForThreads(threads);\n     for (var t in threads){ ?>\n     <item>\n       <title><?= threads[t].getFirstMessageSubject() ?></title>\n       <description><?= messages[t][0].getBody() ?></description>\n       <guid><?= threads[t].getId() ?></guid>\n       <pubDate><?= threads[t].getLastMessageDate().toUTCString() ?></pubDate>\n     </item>\n  <? } ?>\n</channel>\n</rss>"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/AppDelegate.h",
    "content": "#import <UIKit/UIKit.h>\n\n@interface AppDelegate : UIResponder <UIApplicationDelegate>\n\n@property (strong, nonatomic) UIWindow *window;\n\n@end\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/AppDelegate.m",
    "content": "#import \"AppDelegate.h\"\n\n@implementation AppDelegate\n\n@synthesize window = _window;\n\n- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions\n{\n    // Override point for customization after application launch.\n    return YES;\n}\n\t\t\t\t\t\t\t\n- (void)applicationWillResignActive:(UIApplication *)application\n{\n    /*\n     Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.\n     Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.\n     */\n}\n\n- (void)applicationDidEnterBackground:(UIApplication *)application\n{\n    /*\n     Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. \n     If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.\n     */\n}\n\n- (void)applicationWillEnterForeground:(UIApplication *)application\n{\n    /*\n     Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.\n     */\n}\n\n- (void)applicationDidBecomeActive:(UIApplication *)application\n{\n    /*\n     Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.\n     */\n}\n\n- (void)applicationWillTerminate:(UIApplication *)application\n{\n    /*\n     Called when the application is about to terminate.\n     Save data if appropriate.\n     See also applicationDidEnterBackground:.\n     */\n}\n\n@end\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/Inventory-Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>en</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>${PRODUCT_NAME}</string>\n\t<key>CFBundleExecutable</key>\n\t<string>${EXECUTABLE_NAME}</string>\n\t<key>CFBundleIconFiles</key>\n\t<array/>\n\t<key>CFBundleIdentifier</key>\n\t<string>entaq.${PRODUCT_NAME:rfc1034identifier}</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>${PRODUCT_NAME}</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleVersion</key>\n\t<string>1.0</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true/>\n\t<key>UIMainStoryboardFile</key>\n\t<string>MainStoryboard</string>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>armv7</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/Inventory-Prefix.pch",
    "content": "#import <Availability.h>\n\n#ifndef __IPHONE_5_0\n#warning \"This project uses features only available in iOS SDK 5.0 and later.\"\n#endif\n\n#ifdef __OBJC__\n    #import <UIKit/UIKit.h>\n    #import <Foundation/Foundation.h>\n#endif\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/MasterViewController.h",
    "content": "\n#import <UIKit/UIKit.h>\n\n@interface MasterViewController : UITableViewController {\n    NSArray *Items;\n}\n\n- (void)fetchItems;\n- (IBAction)refreshItems:(id)sender;\n\n@end\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/MasterViewController.m",
    "content": "#import \"MasterViewController.h\"\n\n@implementation MasterViewController\n\n\n- (void)awakeFromNib\n{\n    [super awakeFromNib];\n}\n\n- (void)didReceiveMemoryWarning\n{\n    [super didReceiveMemoryWarning];\n    // Release any cached data, images, etc that aren't in use.\n}   \n\n#pragma mark - View lifecycle\n\n- (void)viewDidLoad\n{\n    [super viewDidLoad];\n    [self fetchItems];\n}\n\n- (void)fetchItems\n{\n    NSString *ServiceURL = @\"https://script.google.com/macros/s/AKfycbyonCfUtv-tbIAUO4okOYI8sUCQR-hnJGcOAq8QCQ_86XYc8qg/exec\";\n    \n    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{\n        NSData* data = [NSData dataWithContentsOfURL: \n                        [NSURL URLWithString: ServiceURL]];\n        \n        NSError* error;\n\n        Items = [NSJSONSerialization JSONObjectWithData:data\n                                                 options:kNilOptions \n                                                   error:&error];\n        \n        dispatch_async(dispatch_get_main_queue(), ^{\n            [self.tableView reloadData];\n        });\n    });\n}\n\n- (IBAction)refreshItems:(id)sender {\n    [self fetchItems];\n}\n\n- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section\n{\n    return Items.count;\n}\n\n- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath\n{\n    static NSString *CellIdentifier = @\"ItemCell\";\n    \n    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];\n    if (cell == nil) {\n        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];\n    }\n    \n    NSDictionary *Item = [Items objectAtIndex:indexPath.row];    \n    NSString *text = [Item objectForKey:@\"item\"];\n    NSString *name = [Item objectForKey:@\"quantity\"];\n    \n    cell.textLabel.text = text;\n    cell.detailTextLabel.text = [NSString stringWithFormat:@\"Current quantity %@\", name];\n    \n    return cell;\n}\n\n\n\n- (void)viewDidUnload\n{\n    [super viewDidUnload];\n    // Release any retained subviews of the main view.\n    // e.g. self.myOutlet = nil;\n}\n\n- (void)viewWillAppear:(BOOL)animated\n{\n    [super viewWillAppear:animated];\n}\n\n- (void)viewDidAppear:(BOOL)animated\n{\n    [super viewDidAppear:animated];\n}\n\n- (void)viewWillDisappear:(BOOL)animated\n{\n\t[super viewWillDisappear:animated];\n}\n\n- (void)viewDidDisappear:(BOOL)animated\n{\n\t[super viewDidDisappear:animated];\n}\n\n- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation\n{\n    // Return YES for supported orientations\n    return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown);\n}\n\n@end\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/en.lproj/InfoPlist.strings",
    "content": "/* Localized versions of Info.plist keys */\n\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/en.lproj/MainStoryboard.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"2.0\" toolsVersion=\"2844\" systemVersion=\"12C60\" targetRuntime=\"iOS.CocoaTouch\" propertyAccessControl=\"none\" initialViewController=\"3\">\n    <dependencies>\n        <plugIn identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\" version=\"1930\"/>\n    </dependencies>\n    <scenes>\n        <!--Navigation Controller-->\n        <scene sceneID=\"11\">\n            <objects>\n                <navigationController id=\"3\" sceneMemberID=\"viewController\">\n                    <navigationBar key=\"navigationBar\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"scaleToFill\" id=\"4\">\n                        <autoresizingMask key=\"autoresizingMask\"/>\n                    </navigationBar>\n                    <connections>\n                        <segue destination=\"12\" kind=\"relationship\" relationship=\"rootViewController\" id=\"19\"/>\n                    </connections>\n                </navigationController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"10\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"-1\" y=\"64\"/>\n        </scene>\n        <!--Master View Controller - Master-->\n        <scene sceneID=\"18\">\n            <objects>\n                <tableViewController storyboardIdentifier=\"\" title=\"Master\" id=\"12\" customClass=\"MasterViewController\" sceneMemberID=\"viewController\">\n                    <tableView key=\"view\" opaque=\"NO\" clipsSubviews=\"YES\" clearsContextBeforeDrawing=\"NO\" contentMode=\"scaleToFill\" alwaysBounceVertical=\"YES\" dataMode=\"prototypes\" style=\"plain\" rowHeight=\"44\" sectionHeaderHeight=\"22\" sectionFooterHeight=\"22\" id=\"13\">\n                        <rect key=\"frame\" x=\"0.0\" y=\"64\" width=\"320\" height=\"416\"/>\n                        <autoresizingMask key=\"autoresizingMask\" widthSizable=\"YES\" heightSizable=\"YES\"/>\n                        <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"calibratedWhite\"/>\n                        <prototypes>\n                            <tableViewCell contentMode=\"scaleToFill\" selectionStyle=\"blue\" accessoryType=\"disclosureIndicator\" hidesAccessoryWhenEditing=\"NO\" indentationLevel=\"1\" indentationWidth=\"0.0\" reuseIdentifier=\"ItemCell\" textLabel=\"soZ-1r-uBe\" detailTextLabel=\"LdV-IG-mw5\" style=\"IBUITableViewCellStyleSubtitle\" id=\"8aw-gn-zx0\">\n                                <rect key=\"frame\" x=\"0.0\" y=\"22\" width=\"320\" height=\"44\"/>\n                                <autoresizingMask key=\"autoresizingMask\"/>\n                                <view key=\"contentView\" opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\">\n                                    <rect key=\"frame\" x=\"0.0\" y=\"0.0\" width=\"300\" height=\"43\"/>\n                                    <autoresizingMask key=\"autoresizingMask\"/>\n                                    <subviews>\n                                        <label opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" text=\"Title\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" id=\"soZ-1r-uBe\">\n                                            <rect key=\"frame\" x=\"10\" y=\"2\" width=\"38\" height=\"22\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <fontDescription key=\"fontDescription\" type=\"boldSystem\" pointSize=\"18\"/>\n                                            <color key=\"textColor\" cocoaTouchSystemColor=\"darkTextColor\"/>\n                                            <color key=\"highlightedColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"calibratedRGB\"/>\n                                        </label>\n                                        <label opaque=\"NO\" clipsSubviews=\"YES\" multipleTouchEnabled=\"YES\" contentMode=\"center\" text=\"Subtitle\" lineBreakMode=\"tailTruncation\" baselineAdjustment=\"alignBaselines\" adjustsFontSizeToFit=\"NO\" id=\"LdV-IG-mw5\">\n                                            <rect key=\"frame\" x=\"10\" y=\"24\" width=\"47\" height=\"18\"/>\n                                            <autoresizingMask key=\"autoresizingMask\"/>\n                                            <fontDescription key=\"fontDescription\" type=\"system\" pointSize=\"14\"/>\n                                            <color key=\"textColor\" red=\"0.50196078431372548\" green=\"0.50196078431372548\" blue=\"0.50196078431372548\" alpha=\"1\" colorSpace=\"calibratedRGB\"/>\n                                            <color key=\"highlightedColor\" red=\"1\" green=\"1\" blue=\"1\" alpha=\"1\" colorSpace=\"calibratedRGB\"/>\n                                        </label>\n                                    </subviews>\n                                    <color key=\"backgroundColor\" white=\"0.0\" alpha=\"0.0\" colorSpace=\"calibratedWhite\"/>\n                                </view>\n                                <color key=\"backgroundColor\" white=\"1\" alpha=\"1\" colorSpace=\"calibratedWhite\"/>\n                            </tableViewCell>\n                        </prototypes>\n                        <sections/>\n                        <connections>\n                            <outlet property=\"dataSource\" destination=\"12\" id=\"16\"/>\n                            <outlet property=\"delegate\" destination=\"12\" id=\"15\"/>\n                        </connections>\n                    </tableView>\n                    <navigationItem key=\"navigationItem\" title=\"Inventory\" id=\"35\">\n                        <barButtonItem key=\"rightBarButtonItem\" title=\"Refresh\" id=\"kbX-wS-g6y\">\n                            <connections>\n                                <action selector=\"refreshItems:\" destination=\"12\" id=\"i5P-Jj-KLF\"/>\n                            </connections>\n                        </barButtonItem>\n                    </navigationItem>\n                </tableViewController>\n                <placeholder placeholderIdentifier=\"IBFirstResponder\" id=\"17\" sceneMemberID=\"firstResponder\"/>\n            </objects>\n            <point key=\"canvasLocation\" x=\"455\" y=\"64\"/>\n        </scene>\n    </scenes>\n    <classes>\n        <class className=\"MasterViewController\" superclassName=\"UITableViewController\">\n            <source key=\"sourceIdentifier\" type=\"project\" relativePath=\"./Classes/MasterViewController.h\"/>\n            <relationships>\n                <relationship kind=\"action\" name=\"refreshItems:\"/>\n            </relationships>\n        </class>\n    </classes>\n    <simulatedMetricsContainer key=\"defaultSimulatedMetrics\">\n        <simulatedStatusBarMetrics key=\"statusBar\"/>\n        <simulatedOrientationMetrics key=\"orientation\"/>\n        <simulatedScreenMetrics key=\"destination\"/>\n    </simulatedMetricsContainer>\n</document>"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory/main.m",
    "content": "#import <UIKit/UIKit.h>\n\n#import \"AppDelegate.h\"\n\nint main(int argc, char *argv[])\n{\n    @autoreleasepool {\n        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));\n    }\n}\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 46;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t69BB74F716B2D0C500EC6AF4 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 69BB74F616B2D0C500EC6AF4 /* Default-568h@2x.png */; };\n\t\t6F4DFB9E14BA2B2200B110D2 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F4DFB9D14BA2B2200B110D2 /* UIKit.framework */; };\n\t\t6F4DFBA014BA2B2200B110D2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F4DFB9F14BA2B2200B110D2 /* Foundation.framework */; };\n\t\t6F4DFBA214BA2B2200B110D2 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F4DFBA114BA2B2200B110D2 /* CoreGraphics.framework */; };\n\t\t6F4DFBA814BA2B2200B110D2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6F4DFBA614BA2B2200B110D2 /* InfoPlist.strings */; };\n\t\t6F4DFBAA14BA2B2200B110D2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DFBA914BA2B2200B110D2 /* main.m */; };\n\t\t6F4DFBAE14BA2B2200B110D2 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DFBAD14BA2B2200B110D2 /* AppDelegate.m */; };\n\t\t6F4DFBB114BA2B2200B110D2 /* MainStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6F4DFBAF14BA2B2200B110D2 /* MainStoryboard.storyboard */; };\n\t\t6F4DFBB414BA2B2200B110D2 /* MasterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F4DFBB314BA2B2200B110D2 /* MasterViewController.m */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\t69BB74F616B2D0C500EC6AF4 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = \"Default-568h@2x.png\"; sourceTree = \"<group>\"; };\n\t\t6F4DFB9914BA2B2200B110D2 /* Inventory.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Inventory.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t6F4DFB9D14BA2B2200B110D2 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };\n\t\t6F4DFB9F14BA2B2200B110D2 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };\n\t\t6F4DFBA114BA2B2200B110D2 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };\n\t\t6F4DFBA514BA2B2200B110D2 /* Inventory-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = \"Inventory-Info.plist\"; sourceTree = \"<group>\"; };\n\t\t6F4DFBA714BA2B2200B110D2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = \"<group>\"; };\n\t\t6F4DFBA914BA2B2200B110D2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = \"<group>\"; };\n\t\t6F4DFBAB14BA2B2200B110D2 /* Inventory-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = \"Inventory-Prefix.pch\"; sourceTree = \"<group>\"; };\n\t\t6F4DFBAC14BA2B2200B110D2 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = \"<group>\"; };\n\t\t6F4DFBAD14BA2B2200B110D2 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = \"<group>\"; };\n\t\t6F4DFBB014BA2B2200B110D2 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/MainStoryboard.storyboard; sourceTree = \"<group>\"; };\n\t\t6F4DFBB214BA2B2200B110D2 /* MasterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MasterViewController.h; sourceTree = \"<group>\"; };\n\t\t6F4DFBB314BA2B2200B110D2 /* MasterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MasterViewController.m; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t6F4DFB9614BA2B2200B110D2 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t6F4DFB9E14BA2B2200B110D2 /* UIKit.framework in Frameworks */,\n\t\t\t\t6F4DFBA014BA2B2200B110D2 /* Foundation.framework in Frameworks */,\n\t\t\t\t6F4DFBA214BA2B2200B110D2 /* CoreGraphics.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t6F4DFB8E14BA2B2200B110D2 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t69BB74F616B2D0C500EC6AF4 /* Default-568h@2x.png */,\n\t\t\t\t6F4DFBA314BA2B2200B110D2 /* InventorySample */,\n\t\t\t\t6F4DFB9C14BA2B2200B110D2 /* Frameworks */,\n\t\t\t\t6F4DFB9A14BA2B2200B110D2 /* Products */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6F4DFB9A14BA2B2200B110D2 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6F4DFB9914BA2B2200B110D2 /* Inventory.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6F4DFB9C14BA2B2200B110D2 /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6F4DFB9D14BA2B2200B110D2 /* UIKit.framework */,\n\t\t\t\t6F4DFB9F14BA2B2200B110D2 /* Foundation.framework */,\n\t\t\t\t6F4DFBA114BA2B2200B110D2 /* CoreGraphics.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6F4DFBA314BA2B2200B110D2 /* InventorySample */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6F4DFBAC14BA2B2200B110D2 /* AppDelegate.h */,\n\t\t\t\t6F4DFBAD14BA2B2200B110D2 /* AppDelegate.m */,\n\t\t\t\t6F4DFBAF14BA2B2200B110D2 /* MainStoryboard.storyboard */,\n\t\t\t\t6F4DFBB214BA2B2200B110D2 /* MasterViewController.h */,\n\t\t\t\t6F4DFBB314BA2B2200B110D2 /* MasterViewController.m */,\n\t\t\t\t6F4DFBA414BA2B2200B110D2 /* Supporting Files */,\n\t\t\t);\n\t\t\tname = InventorySample;\n\t\t\tpath = Inventory;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6F4DFBA414BA2B2200B110D2 /* Supporting Files */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t6F4DFBA514BA2B2200B110D2 /* Inventory-Info.plist */,\n\t\t\t\t6F4DFBA614BA2B2200B110D2 /* InfoPlist.strings */,\n\t\t\t\t6F4DFBA914BA2B2200B110D2 /* main.m */,\n\t\t\t\t6F4DFBAB14BA2B2200B110D2 /* Inventory-Prefix.pch */,\n\t\t\t);\n\t\t\tname = \"Supporting Files\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t6F4DFB9814BA2B2200B110D2 /* Inventory */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 6F4DFBBA14BA2B2200B110D2 /* Build configuration list for PBXNativeTarget \"Inventory\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t6F4DFB9514BA2B2200B110D2 /* Sources */,\n\t\t\t\t6F4DFB9614BA2B2200B110D2 /* Frameworks */,\n\t\t\t\t6F4DFB9714BA2B2200B110D2 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = Inventory;\n\t\t\tproductName = \"Twitter Test\";\n\t\t\tproductReference = 6F4DFB9914BA2B2200B110D2 /* Inventory.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t6F4DFB9014BA2B2200B110D2 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastUpgradeCheck = 0420;\n\t\t\t};\n\t\t\tbuildConfigurationList = 6F4DFB9314BA2B2200B110D2 /* Build configuration list for PBXProject \"Inventory\" */;\n\t\t\tcompatibilityVersion = \"Xcode 3.2\";\n\t\t\tdevelopmentRegion = English;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t);\n\t\t\tmainGroup = 6F4DFB8E14BA2B2200B110D2;\n\t\t\tproductRefGroup = 6F4DFB9A14BA2B2200B110D2 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t6F4DFB9814BA2B2200B110D2 /* Inventory */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t6F4DFB9714BA2B2200B110D2 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t6F4DFBA814BA2B2200B110D2 /* InfoPlist.strings in Resources */,\n\t\t\t\t6F4DFBB114BA2B2200B110D2 /* MainStoryboard.storyboard in Resources */,\n\t\t\t\t69BB74F716B2D0C500EC6AF4 /* Default-568h@2x.png in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t6F4DFB9514BA2B2200B110D2 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t6F4DFBAA14BA2B2200B110D2 /* main.m in Sources */,\n\t\t\t\t6F4DFBAE14BA2B2200B110D2 /* AppDelegate.m in Sources */,\n\t\t\t\t6F4DFBB414BA2B2200B110D2 /* MasterViewController.m in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t6F4DFBA614BA2B2200B110D2 /* InfoPlist.strings */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t6F4DFBA714BA2B2200B110D2 /* en */,\n\t\t\t);\n\t\t\tname = InfoPlist.strings;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t6F4DFBAF14BA2B2200B110D2 /* MainStoryboard.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t6F4DFBB014BA2B2200B110D2 /* en */,\n\t\t\t);\n\t\t\tname = MainStoryboard.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t6F4DFBB814BA2B2200B110D2 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tARCHS = \"$(ARCHS_STANDARD_32_BIT)\";\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_SYMBOLS_PRIVATE_EXTERN = NO;\n\t\t\t\tGCC_VERSION = com.apple.compilers.llvm.clang.1_0;\n\t\t\t\tGCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 5.0;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t6F4DFBB914BA2B2200B110D2 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tARCHS = \"$(ARCHS_STANDARD_32_BIT)\";\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\t\"CODE_SIGN_IDENTITY[sdk=iphoneos*]\" = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu99;\n\t\t\t\tGCC_VERSION = com.apple.compilers.llvm.clang.1_0;\n\t\t\t\tGCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 5.0;\n\t\t\t\tOTHER_CFLAGS = \"-DNS_BLOCK_ASSERTIONS=1\";\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t6F4DFBBB14BA2B2200B110D2 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tGCC_PRECOMPILE_PREFIX_HEADER = YES;\n\t\t\t\tGCC_PREFIX_HEADER = \"Inventory/Inventory-Prefix.pch\";\n\t\t\t\tINFOPLIST_FILE = \"$(SRCROOT)/Inventory/Inventory-Info.plist\";\n\t\t\t\tPRODUCT_NAME = Inventory;\n\t\t\t\tWRAPPER_EXTENSION = app;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t6F4DFBBC14BA2B2200B110D2 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tGCC_PRECOMPILE_PREFIX_HEADER = YES;\n\t\t\t\tGCC_PREFIX_HEADER = \"Inventory/Inventory-Prefix.pch\";\n\t\t\t\tINFOPLIST_FILE = \"$(SRCROOT)/Inventory/Inventory-Info.plist\";\n\t\t\t\tPRODUCT_NAME = Inventory;\n\t\t\t\tWRAPPER_EXTENSION = app;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t6F4DFB9314BA2B2200B110D2 /* Build configuration list for PBXProject \"Inventory\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t6F4DFBB814BA2B2200B110D2 /* Debug */,\n\t\t\t\t6F4DFBB914BA2B2200B110D2 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t6F4DFBBA14BA2B2200B110D2 /* Build configuration list for PBXNativeTarget \"Inventory\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t6F4DFBBB14BA2B2200B110D2 /* Debug */,\n\t\t\t\t6F4DFBBC14BA2B2200B110D2 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 6F4DFB9014BA2B2200B110D2 /* Project object */;\n}\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:Inventory.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory.xcodeproj/project.xcworkspace/xcuserdata/anagarajan.xcuserdatad/WorkspaceSettings.xcsettings",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges</key>\n\t<true/>\n\t<key>SnapshotAutomaticallyBeforeSignificantChanges</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory.xcodeproj/xcuserdata/anagarajan.xcuserdatad/xcdebugger/Breakpoints.xcbkptlist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Bucket\n   type = \"1\"\n   version = \"1.0\">\n</Bucket>\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory.xcodeproj/xcuserdata/anagarajan.xcuserdatad/xcschemes/InventorySample.xcscheme",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n   LastUpgradeVersion = \"0450\"\n   version = \"1.3\">\n   <BuildAction\n      parallelizeBuildables = \"YES\"\n      buildImplicitDependencies = \"YES\">\n      <BuildActionEntries>\n         <BuildActionEntry\n            buildForTesting = \"YES\"\n            buildForRunning = \"YES\"\n            buildForProfiling = \"YES\"\n            buildForArchiving = \"YES\"\n            buildForAnalyzing = \"YES\">\n            <BuildableReference\n               BuildableIdentifier = \"primary\"\n               BlueprintIdentifier = \"6F4DFB9814BA2B2200B110D2\"\n               BuildableName = \"Inventory.app\"\n               BlueprintName = \"Inventory\"\n               ReferencedContainer = \"container:Inventory.xcodeproj\">\n            </BuildableReference>\n         </BuildActionEntry>\n      </BuildActionEntries>\n   </BuildAction>\n   <TestAction\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      buildConfiguration = \"Debug\">\n      <Testables>\n      </Testables>\n      <MacroExpansion>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"6F4DFB9814BA2B2200B110D2\"\n            BuildableName = \"Inventory.app\"\n            BlueprintName = \"Inventory\"\n            ReferencedContainer = \"container:Inventory.xcodeproj\">\n         </BuildableReference>\n      </MacroExpansion>\n   </TestAction>\n   <LaunchAction\n      selectedDebuggerIdentifier = \"Xcode.DebuggerFoundation.Debugger.LLDB\"\n      selectedLauncherIdentifier = \"Xcode.DebuggerFoundation.Launcher.LLDB\"\n      launchStyle = \"0\"\n      useCustomWorkingDirectory = \"NO\"\n      buildConfiguration = \"Debug\"\n      ignoresPersistentStateOnLaunch = \"NO\"\n      debugDocumentVersioning = \"YES\"\n      allowLocationSimulation = \"YES\">\n      <BuildableProductRunnable>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"6F4DFB9814BA2B2200B110D2\"\n            BuildableName = \"Inventory.app\"\n            BlueprintName = \"Inventory\"\n            ReferencedContainer = \"container:Inventory.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n      <AdditionalOptions>\n      </AdditionalOptions>\n   </LaunchAction>\n   <ProfileAction\n      shouldUseLaunchSchemeArgsEnv = \"YES\"\n      savedToolIdentifier = \"\"\n      useCustomWorkingDirectory = \"NO\"\n      buildConfiguration = \"Release\"\n      debugDocumentVersioning = \"YES\">\n      <BuildableProductRunnable>\n         <BuildableReference\n            BuildableIdentifier = \"primary\"\n            BlueprintIdentifier = \"6F4DFB9814BA2B2200B110D2\"\n            BuildableName = \"Inventory.app\"\n            BlueprintName = \"Inventory\"\n            ReferencedContainer = \"container:Inventory.xcodeproj\">\n         </BuildableReference>\n      </BuildableProductRunnable>\n   </ProfileAction>\n   <AnalyzeAction\n      buildConfiguration = \"Debug\">\n   </AnalyzeAction>\n   <ArchiveAction\n      buildConfiguration = \"Release\"\n      revealArchiveInOrganizer = \"YES\">\n   </ArchiveAction>\n</Scheme>\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/Inventory-iOS/Inventory.xcodeproj/xcuserdata/anagarajan.xcuserdatad/xcschemes/xcschememanagement.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>SchemeUserState</key>\n\t<dict>\n\t\t<key>InventorySample.xcscheme</key>\n\t\t<dict>\n\t\t\t<key>orderHint</key>\n\t\t\t<integer>0</integer>\n\t\t</dict>\n\t</dict>\n\t<key>SuppressBuildableAutocreation</key>\n\t<dict>\n\t\t<key>6F4DFB9814BA2B2200B110D2</key>\n\t\t<dict>\n\t\t\t<key>primary</key>\n\t\t\t<true/>\n\t\t</dict>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "ContentService/Inventory-JSON-API-iOS/code.gs",
    "content": "function doGet() {\n  var ss = SpreadsheetApp.openById('0AiFUpEyZ-UKzdElxM3NpbFI4QUJwdmxqSXVlSjJmNmc');\n  var sheet = ss.getSheets()[0];\n\n  // Get the range of cells that store employee data.\n  var employeeDataRange = ss.getRangeByName(\"inventoryData\");\n\n  // For every row of employee data, generate an employee object.\n  var employeeObjects = getRowsData(sheet, employeeDataRange);\n  return ContentService.createTextOutput(JSON.stringify(employeeObjects)).setMimeType(ContentService.MimeType.JSON);\n}\n\n\n// getRowsData iterates row by row in the input range and returns an array of objects.\n// Each object contains all the data for a given row, indexed by its normalized column name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - columnHeadersRowIndex: specifies the row number where the column names are stored.\n//       This argument is optional and it defaults to the row immediately above range; \n// Returns an Array of objects.\nfunction getRowsData(sheet, range, columnHeadersRowIndex) {\n  columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;\n  var numColumns = range.getLastColumn() - range.getColumn() + 1;\n  var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);\n  var headers = headersRange.getValues()[0];\n  return getObjects(range.getValues(), normalizeHeaders(headers));\n}\n\n\n// For every row of data in data, generates an object that contains the data. Names of\n// object fields are defined in keys.\n// Arguments:\n//   - data: JavaScript 2d array\n//   - keys: Array of Strings that define the property names for the objects to create\nfunction getObjects(data, keys) {\n  var objects = [];\n  for (var i = 0; i < data.length; ++i) {\n    var object = {};\n    var hasData = false;\n    for (var j = 0; j < data[i].length; ++j) {\n      var cellData = data[i][j];\n      if (isCellEmpty(cellData)) {\n        continue;\n      }\n      object[keys[j]] = cellData;\n      hasData = true;\n    }\n    if (hasData) {\n      objects.push(object);\n    }\n  }\n  return objects;\n}\n\n// Returns an Array of normalized Strings.\n// Arguments:\n//   - headers: Array of Strings to normalize\nfunction normalizeHeaders(headers) {\n  var keys = [];\n  for (var i = 0; i < headers.length; ++i) {\n    var key = normalizeHeader(headers[i]);\n    if (key.length > 0) {\n      keys.push(key);\n    }\n  }\n  return keys;\n}\n\n// Normalizes a string, by removing all alphanumeric characters and using mixed case\n// to separate words. The output will always start with a lower case letter.\n// This function is designed to produce JavaScript object property names.\n// Arguments:\n//   - header: string to normalize\n// Examples:\n//   \"First Name\" -> \"firstName\"\n//   \"Market Cap (millions) -> \"marketCapMillions\n//   \"1 number at the beginning is ignored\" -> \"numberAtTheBeginningIsIgnored\"\nfunction normalizeHeader(header) {\n  var key = \"\";\n  var upperCase = false;\n  for (var i = 0; i < header.length; ++i) {\n    var letter = header[i];\n    if (letter == \" \" && key.length > 0) {\n      upperCase = true;\n      continue;\n    }\n    if (!isAlnum(letter)) {\n      continue;\n    }\n    if (key.length == 0 && isDigit(letter)) {\n      continue; // first character must be a letter\n    }\n    if (upperCase) {\n      upperCase = false;\n      key += letter.toUpperCase();\n    } else {\n      key += letter.toLowerCase();\n    }\n  }\n  return key;\n}\n\n// Returns true if the cell where cellData was read from is empty.\n// Arguments:\n//   - cellData: string\nfunction isCellEmpty(cellData) {\n  return typeof(cellData) == \"string\" && cellData == \"\";\n}\n\n// Returns true if the character char is alphabetical, false otherwise.\nfunction isAlnum(char) {\n  return char >= 'A' && char <= 'Z' ||\n    char >= 'a' && char <= 'z' ||\n    isDigit(char);\n}\n\n// Returns true if the character char is a digit, false otherwise.\nfunction isDigit(char) {\n  return char >= '0' && char <= '9';\n}\n\n// Given a JavaScript 2d Array, this function returns the transposed table.\n// Arguments:\n//   - data: JavaScript 2d Array\n// Returns a JavaScript 2d Array\n// Example: arrayTranspose([[1,2,3],[4,5,6]]) returns [[1,4],[2,5],[3,6]].\nfunction arrayTranspose(data) {\n  if (data.length == 0 || data[0].length == 0) {\n    return null;\n  }\n\n  var ret = [];\n  for (var i = 0; i < data[0].length; ++i) {\n    ret.push([]);\n  }\n\n  for (var i = 0; i < data.length; ++i) {\n    for (var j = 0; j < data[i].length; ++j) {\n      ret[j][i] = data[i][j];\n    }\n  }\n\n  return ret;\n}\n\n\n\n\n// Exercise:\n\n\nfunction runExercise() {\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  var sheet = ss.getSheets()[1];\n\n  // Get the range of cells that store employee data.\n  var employeeDataRange = sheet.getRange(\"B1:F5\");\n\n  // For every row of employee data, generate an employee object.\n  var employeeObjects = getColumnsData(sheet, employeeDataRange);\n\n  var thirdEmployee = employeeObjects[2];\n  var stringToDisplay = \"The third column is: \" + thirdEmployee.firstName + \" \" + thirdEmployee.lastName;\n  stringToDisplay += \" (id #\" + thirdEmployee.employeeId + \") working in the \";\n  stringToDisplay += thirdEmployee.department + \" department and with phone number \";\n  stringToDisplay += thirdEmployee.phoneNumber;\n  ss.msgBox(stringToDisplay);\n}\n\n// Given a JavaScript 2d Array, this function returns the transposed table.\n// Arguments:\n//   - data: JavaScript 2d Array\n// Returns a JavaScript 2d Array\n// Example: arrayTranspose([[1,2,3],[4,5,6]]) returns [[1,4],[2,5],[3,6]].\nfunction arrayTranspose(data) {\n  if (data.length == 0 || data[0].length == 0) {\n    return null;\n  }\n\n  var ret = [];\n  for (var i = 0; i < data[0].length; ++i) {\n    ret.push([]);\n  }\n\n  for (var i = 0; i < data.length; ++i) {\n    for (var j = 0; j < data[i].length; ++j) {\n      ret[j][i] = data[i][j];\n    }\n  }\n\n  return ret;\n}\n\n// getColumnsData iterates column by column in the input range and returns an array of objects.\n// Each object contains all the data for a given column, indexed by its normalized row name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - rowHeadersColumnIndex: specifies the column number where the row names are stored.\n//       This argument is optional and it defaults to the column immediately left of the range; \n// Returns an Array of objects.\nfunction getColumnsData(sheet, range, rowHeadersColumnIndex) {\n  rowHeadersColumnIndex = rowHeadersColumnIndex || range.getColumnIndex() - 1;\n  var headersTmp = sheet.getRange(range.getRow(), rowHeadersColumnIndex, range.getNumRows(), 1).getValues();\n  var headers = normalizeHeaders(arrayTranspose(headersTmp)[0]);\n  return getObjects(arrayTranspose(range.getValues()), headers);\n}"
  },
  {
    "path": "ContentService/Simple-JSONP/SimpleHtml-JSFiddle.html",
    "content": "<!DOCTYPE html>\n<html>\n \t<head>\n    \t<meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\"/>\n    \t<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js\"></script>\n\t\t<script>\n\t\t\t$(document).ready(function () {    \n\t\t\t\t$.getJSON('https://script.google.com/macros/s/AKfycbzhzyGY5nCLdho3RBmWDLzDX6Rvdv6Uj4qpe83vTNnxICSILbfk/exec?prefix=?', null, function (results) {\n\t\t\t\t\t$('h1').html(results);\n\t\t\t\t});\n\t\t\t});\n\t\t\t</script>\n\t</head>\n\t<body>\n    \t<h1>welcome<h1>\n\t</body>\n</html>\n"
  },
  {
    "path": "ContentService/Simple-JSONP/code.gs",
    "content": "function doGet(request) {\n  var result = 'hello world';\n  var content = request.parameters.prefix + '(' +JSON.stringify(result) + ')';\n  return ContentService.createTextOutput(content)\n    .setMimeType(ContentService.MimeType.JSON);\n}\n"
  },
  {
    "path": "ContentService/readme.md",
    "content": "Click the image below for video with more details - \n\n[![ScreenShot](https://i.ytimg.com/vi/JRGzVdliQOQ/hqdefault.jpg)](http://www.youtube.com/watch?v=JRGzVdliQOQ&list=PL68F511F6E3C122EB)"
  },
  {
    "path": "DriveSDK/AppsScript.gs",
    "content": "//look for the first mtg right now in the user's calendar from now till next 2 hours\nfunction createMeetingNotes() {\n  var lowerBound = new Date();\n  var upperBound = new Date();\n  upperBound.setHours(lowerBound.getHours()+2);\n  \n  var appts = CalendarApp.getEvents(lowerBound, upperBound);\n  if(!appts || appts.length ==0){\n    Logger.log('No current appointments!');\n    return '';\n  }\n  \n  //naive implementation for demo - ideally we'll provide a picker if double booked\n  var appt = appts[0];\n  var doc = DocumentApp.create('Meeting notes for ' + appt.getTitle());\n  doc.appendParagraph('Time: ' + appt.getStartTime());\n  doc.appendParagraph('Location: ' + appt.getLocation());\n  doc.appendParagraph('Attendees: ' + getEmails(appt));\n  doc.appendHorizontalRule();\n  doc.appendParagraph(appt.getDescription());\n  return doc.getUrl();\n}\n\nfunction getEmails(appt){\n  var guests = appt.getGuestList(true);\n  var emails = '';\n  for(var i=0;i<guests.length;i++){\n    emails += guests[i].getEmail() + ', ';\n  }\n  return emails.slice(0,-2);\n}\n\nfunction zipAndSend(fileIds,emailAddress){\n  var names = {};\n  var zipFile = Utilities.zip(fileIds.map(function(i){\n    var f = DocsList.getFileById(i);\n    Logger.log(f.getName());\n    var n = f.getName() + '.pdf';\n    while (names[n]) { n = '_' + n }\n    names[n] = true;\n    return f.getAs('application/pdf').setName(n);\n  }), 'FilesForYou.zip')\n  MailApp.sendEmail(emailAddress, 'Files you requested', 'Attached is the ZIP of the pdf documents we discussed', {attachments: [zipFile]});\n}"
  },
  {
    "path": "DriveSDK/ChromeWebStoreVersion/SimpleZipAndSend.gs",
    "content": "//this is the source code for \"Zip and send\" app on the Chrome Web Store you can install into your Google Drive\nfunction doGet(e) {\n  var state = JSON.parse(e.parameters.state);\n  zipAndSend(state.exportIds,Session.getEffectiveUser().getEmail());\n  return HtmlService.createHtmlOutput(\"<html><h1>Email sent. Check your inbox!</h1></html>\");\n}\n\nfunction zipAndSend(fileIds,emailAddress){\n  var names = {};\n  var zipFile = Utilities.zip(fileIds.map(function(i){\n    var f = DocsList.getFileById(i);\n    Logger.log(f.getName());\n    var n = f.getName() + '.pdf';\n    while (names[n]) { n = '_' + n }\n    names[n] = true;\n    return f.getAs('application/pdf').setName(n);\n  }), 'FilesForYou.zip')\n  MailApp.sendEmail(emailAddress, 'Files you requested', 'Attached is the ZIP of the pdf documents we discussed', {attachments: [zipFile]});\n}\n"
  },
  {
    "path": "DriveSDK/FlowManager.gs",
    "content": "var AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth'; \nvar TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'; \n\nvar REDIRECT_URL= 'YOUR_REDIRECT_URL';//ScriptApp.getService().getUrl();\nvar tokenPropertyName = 'GOOGLE_OAUTH_TOKEN'; \n\nvar CLIENT_ID = 'YOUR_CLIENT_ID';\nvar CLIENT_SECRET = 'YOUR_CLIENT_SECRET';\n\nfunction doGet(e) {\n  var HTMLToOutput;\n  \n  if(e.parameters.state){\n    var state = JSON.parse(e.parameters.state);\n    if(state.action === 'create'){\n      var meetingURL = createMeetingNotes();\n      HTMLToOutput = \"<html><h1>Meeting notes document created!</h1><a href='\"+meetingURL+\"'>click here to open</a></html>\"; \n    }\n    else {\n      zipAndSend(state.exportIds,Session.getEffectiveUser().getEmail());\n      HTMLToOutput = \"<html><h1>Email sent. Check your inbox!</h1></html>\"; \n    }\n  }\n  else if(e.parameters.code){//if we get \"code\" as a parameter in, then this is a callback. we can make this more explicit\n    getAndStoreAccessToken(e.parameters.code);\n    HTMLToOutput = '<html><h1>App is installed, you can close this window now or navigate to your <a href=\"https://drive.google.com\">Google Drive</a>.</h1></html>';\n  }\n  else {//we are starting from scratch or resetting\n    HTMLToOutput = \"<html><h1>Install this App into your Google Drive!</h1><a href='\"+getURLForAuthorization()+\"'>click here to start</a></html>\";\n  }\n  return HtmlService.createHtmlOutput(HTMLToOutput);\n}\n\nfunction getURLForAuthorization(){\n  return AUTHORIZE_URL + '?response_type=code&client_id='+CLIENT_ID+'&redirect_uri='+REDIRECT_URL +\n    '&scope=https://www.googleapis.com/auth/drive.install https://www.googleapis.com/auth/userinfo.email';  \n}\n\nfunction getAndStoreAccessToken(code){\n  var parameters = { method : 'post',\n                    payload : 'client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&grant_type=authorization_code&redirect_uri='+REDIRECT_URL+'&code=' + code};\n  \n  var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText();   \n  var tokenResponse = JSON.parse(response);\n  UserProperties.setProperty(tokenPropertyName, tokenResponse.access_token);\n}\n\nfunction getUrlFetchOptions() {\n  return {'contentType' : 'application/json',\n          'headers' : {'Authorization' : 'Bearer ' + UserProperties.getProperty(tokenPropertyName),\n                       'Accept' : 'application/json'}};\n}\n\n//naive check, not using for now, use refresh tokens and add proper checking\nfunction isTokenValid() {\n  return UserProperties.getProperty(tokenPropertyName);\n}\n\n"
  },
  {
    "path": "DriveSDK/readme.md",
    "content": "Click the image below for video with more details - \n\n[![ScreenShot](https://i.ytimg.com/vi/R71oo-5NmPE/hqdefault.jpg)](http://www.youtube.com/watch?v=R71oo-5NmPE&list=PL68F511F6E3C122EB)"
  },
  {
    "path": "FusionTables/RowUtilities.gs",
    "content": "// getRowsData iterates row by row in the input range and returns an array of objects.\n// Each object contains all the data for a given row, indexed by its normalized column name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - columnHeadersRowIndex: specifies the row number where the column names are stored.\n//       This argument is optional and it defaults to the row immediately above range;\n// Returns an Array of objects.\nfunction getRowsData(sheet, range, columnHeadersRowIndex) {\n  columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;\n  var numColumns = range.getEndColumn() - range.getColumn() + 1;\n  var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);\n  var headers = headersRange.getValues()[0];\n  return getObjects(range.getValues(), normalizeHeaders(headers));\n}\n\n// getColumnsData iterates column by column in the input range and returns an array of objects.\n// Each object contains all the data for a given column, indexed by its normalized row name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - rowHeadersColumnIndex: specifies the column number where the row names are stored.\n//       This argument is optional and it defaults to the column immediately left of the range;\n// Returns an Array of objects.\nfunction getColumnsData(sheet, range, rowHeadersColumnIndex) {\n  rowHeadersColumnIndex = rowHeadersColumnIndex || range.getColumnIndex() - 1;\n  var headersTmp = sheet.getRange(range.getRow(), rowHeadersColumnIndex, range.getNumRows(), 1).getValues();\n  var headers = normalizeHeaders(arrayTranspose(headersTmp)[0]);\n  return getObjects(arrayTranspose(range.getValues()), headers);\n}\n\n\n// For every row of data in data, generates an object that contains the data. Names of\n// object fields are defined in keys.\n// Arguments:\n//   - data: JavaScript 2d array\n//   - keys: Array of Strings that define the property names for the objects to create\nfunction getObjects(data, keys) {\n  var objects = [];\n  for (var i = 0; i < data.length; ++i) {\n    var object = {};\n    var hasData = false;\n    for (var j = 0; j < data[i].length; ++j) {\n      var cellData = data[i][j];\n      if (isCellEmpty(cellData)) {\n        continue;\n      }\n      object[keys[j]] = cellData;\n      hasData = true;\n    }\n    if (hasData) {\n      objects.push(object);\n    }\n  }\n  return objects;\n}\n\n// Returns an Array of normalized Strings.\n// Arguments:\n//   - headers: Array of Strings to normalize\nfunction normalizeHeaders(headers) {\n  var keys = [];\n  for (var i = 0; i < headers.length; ++i) {\n    var key = normalizeHeader(headers[i]);\n    if (key.length > 0) {\n      keys.push(key);\n    }\n  }\n  return keys;\n}\n\n// Normalizes a string, by removing all alphanumeric characters and using mixed case\n// to separate words. The output will always start with a lower case letter.\n// This function is designed to produce JavaScript object property names.\n// Arguments:\n//   - header: string to normalize\n// Examples:\n//   \"First Name\" -> \"firstName\"\n//   \"Market Cap (millions) -> \"marketCapMillions\n//   \"1 number at the beginning is ignored\" -> \"numberAtTheBeginningIsIgnored\"\nfunction normalizeHeader(header) {\n  var key = \"\";\n  var upperCase = false;\n  for (var i = 0; i < header.length; ++i) {\n    var letter = header[i];\n    if (letter == \" \" && key.length > 0) {\n      upperCase = true;\n      continue;\n    }\n    if (!isAlnum(letter)) {\n      continue;\n    }\n    if (key.length == 0 && isDigit(letter)) {\n      continue; // first character must be a letter\n    }\n    if (upperCase) {\n      upperCase = false;\n      key += letter.toUpperCase();\n    } else {\n      key += letter.toLowerCase();\n    }\n  }\n  return key;\n}\n\n// Returns true if the cell where cellData was read from is empty.\n// Arguments:\n//   - cellData: string\nfunction isCellEmpty(cellData) {\n  return typeof(cellData) == \"string\" && cellData == \"\";\n}\n\n// Returns true if the character char is alphabetical, false otherwise.\nfunction isAlnum(char) {\n  return char >= 'A' && char <= 'Z' ||\n    char >= 'a' && char <= 'z' ||\n    isDigit(char);\n}\n\n// Returns true if the character char is a digit, false otherwise.\nfunction isDigit(char) {\n  return char >= '0' && char <= '9';\n}\n\n// Given a JavaScript 2d Array, this function returns the transposed table.\n// Arguments:\n//   - data: JavaScript 2d Array\n// Returns a JavaScript 2d Array\n// Example: arrayTranspose([[1,2,3],[4,5,6]]) returns [[1,4],[2,5],[3,6]].\nfunction arrayTranspose(data) {\n  if (data.length == 0 || data[0].length == 0) {\n    return null;\n  }\n\n  var ret = [];\n  for (var i = 0; i < data[0].length; ++i) {\n    ret.push([]);\n  }\n\n  for (var i = 0; i < data.length; ++i) {\n    for (var j = 0; j < data[i].length; ++j) {\n      ret[j][i] = data[i][j];\n    }\n  }\n\n  return ret;\n}"
  },
  {
    "path": "FusionTables/SampleDataExchange.gs",
    "content": "function onInstall(){\n  onOpen();\n}\n\nvar ss = SpreadsheetApp.getActiveSpreadsheet();\n\nfunction onOpen() {\n  var menuEntries = [{name: \"Login\", functionName: \"login\"},\n                     {name: \"Download from Fusion Tables\", functionName: \"downloadFromFusionTables\"},\n                     {name: \"Upload to Fusion Tables\", functionName: \"uploadToFusionTables\"},\n                     {name: \"Reset credentials\", functionName: \"clearCreds\"}];\n  ss.addMenu(\"Fusion Tables\", menuEntries);\n}\n\nfunction login(){\n  if(isTokenValid()){\n    HTMLToOutput = '<html><h1>Already have token</h1></html>';\n  }\n  else {//we are starting from scratch or resetting\n    HTMLToOutput = \"<html><h1>You need to login</h1><a href='\"+getURLForAuthorization()+\"'>click here to start</a><br>Re-open this window when you return.</html>\";\n  }\n  ss.show(HtmlService.createHtmlOutput(HTMLToOutput));\n}\n\nfunction doGet(e) {\n  var HTMLToOutput;\n  if(e.parameters.code){//if we get \"code\" as a parameter in, then this is a callback. we can make this more explicit\n    getAndStoreAccessToken(e.parameters.code);\n    HTMLToOutput = '<html><h1>Finished with oAuth</h1>You can close this window.</html>';\n  }\n  return HtmlService.createHtmlOutput(HTMLToOutput);\n}\nvar tableName = '1ETYzpkpTm4SfOMFTWHYgT9q1s4mSIE87yQCnQ04';\n\nfunction downloadFromFusionTables(){\n  var schoolName = lookupSchoolName();\n  \n  var dataResponse = runSQL(\"select ROWID,StudentID,Subject,Grade,School from \"+tableName+\" where School = '\"+schoolName+\"'\");\n  var respObject = JSON.parse(dataResponse);\n  ss.appendRow(respObject.columns);\n  for(var i in respObject.rows){\n    ss.appendRow(respObject.rows[i]);\n  }\n  ss.getActiveSheet().clearNotes();\n}\n\nfunction runSQL(sql){\n  var getDataURL = 'https://www.googleapis.com/fusiontables/v1/query?sql='+sql;\n  var dataResponse = UrlFetchApp.fetch(getDataURL,getUrlFetchOptions()).getContentText();  \n  return dataResponse;\n}\n\nvar tempEditedProperty = 'CURRENT_EDITED';\n\n//simple tracker to see what changed. need to add more validation logic and safety checks here. \nfunction onEdit(e){   \n  e.range.setComment(\"Edited at: \" + new Date().toTimeString());\n  var rowid = ss.getActiveSheet().getRange(e.range.getRow(),1).getValue();\n  var currentEditedItems = UserProperties.getProperty(tempEditedProperty)  || '';\n  currentEditedItems += rowid+',';\n  UserProperties.setProperty(tempEditedProperty,currentEditedItems);\n}\n\nfunction uploadToFusionTables() {\n  var currentEditedItems = UserProperties.getProperty(tempEditedProperty)  || '';\n  var rowIdsEdited = currentEditedItems.split(',');\n  var rowObjects = getRowsData(ss.getActiveSheet(), ss.getDataRange(),1);\n  var runningLog = '';\n  for(var i = 1;i<rowObjects.length;i++){\n    if(rowIdsEdited.indexOf(String(rowObjects[i].rowid))>-1){\n      runningLog += 'updating rowid = ' + rowObjects[i].rowid + ' with grade of ' + rowObjects[i].grade + '. ';\n      runSQL(\"update \"+tableName+\" SET Grade = \"+rowObjects[i].grade+\" WHERE ROWID = '\"+String(rowObjects[i].rowid)+\"'\");\n    }\n  }\n  UserProperties.deleteProperty(tempEditedProperty);\n  ss.getActiveSheet().clearNotes();\n  if(runningLog){\n    Browser.msgBox(runningLog);\n  }\n}\n\nvar AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth'; //step 1. we can actually start directly here if that is necessary\nvar TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'; //step 2. after we get the callback, go get token\n\n\n//PUT YOUR OWN SETTINGS HERE\nvar CLIENT_ID = 'YOUR_CLIENT_ID';\nvar CLIENT_SECRET='YOUR_CLIENT_SECRET';\nvar REDIRECT_URL= 'YOUR_REDIRECT_URL';\n\nvar tokenPropertyName = 'FUSIONTABLES_OAUTH_TOKEN'; \n\nfunction getURLForAuthorization(){\n  return AUTHORIZE_URL + '?response_type=code&client_id='+CLIENT_ID+'&redirect_uri='+REDIRECT_URL +\n    '&scope=https://www.googleapis.com/auth/fusiontables&state=/profile';  \n}\n\nfunction getAndStoreAccessToken(code){\n  var parameters = {\n    method : 'post',\n    payload : 'client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&grant_type=authorization_code&redirect_uri='+REDIRECT_URL+'&code=' + code\n  };\n  \n  var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText();   \n  var tokenResponse = JSON.parse(response);\n  \n  //store the token for later retrival\n  UserProperties.setProperty(tokenPropertyName, tokenResponse.access_token);\n}\n\nfunction getUrlFetchOptions() {\n  var token = UserProperties.getProperty(tokenPropertyName);\n  return {\n    \"method\" : \"post\",\n    \"contentType\" : \"application/json\",\n    \"headers\" : {\n      \"Authorization\" : \"Bearer \" + token,\n      \"Accept\" : \"application/json\"\n    }\n  };\n}\n\n// we don't have a logout option here. for now, manually clear out the token under File->Project->User Properties\nfunction isTokenValid() {\n  var token = UserProperties.getProperty(tokenPropertyName);\n  if(!token){ //if its empty or undefined\n    return false;\n  }\n  return true; //naive check\n}\n\n\nfunction clearCreds(){\n  UserProperties.deleteProperty(tokenPropertyName); \n  Browser.msgBox('Tokens cleared. Please login again from the menu before using Fusion tables');\n}\n\n\n var schoolName = {\n    \"tom.principal@acmemiddleschool.edu\":'Acme Middle School',\n    \"arun.at.pyxis@gmail.com\":'Short Hills Prep',\n    \"arun.appsscript@gmail.com\":'PK Middle School'\n  }\nvar user = Session.getEffectiveUser().getEmail();\n\nfunction lookupSchoolName(){\n  return schoolName[user] || schoolName[0];\n}\n\n"
  },
  {
    "path": "FusionTables/readme.md",
    "content": "Click the image below for video with more details - \n\n[![ScreenShot](https://i.ytimg.com/vi/eHM0zbUE5A0/hqdefault.jpg)](http://www.youtube.com/watch?v=eHM0zbUE5A0&list=PL68F511F6E3C122EB)"
  },
  {
    "path": "GoogleAppsAdminAudit.gs",
    "content": "function doGet(e) {\n  var HTMLToOutput;\n  if(e.parameters.code){//if we get \"code\" as a parameter in, then this is a callback. we can make this more explicit\n    getAndStoreAccessToken(e.parameters.code);\n    HTMLToOutput = '<html><h1>Finished with oAuth</h1></html>';\n  }\n  else if(isTokenValid()){//if we already have a valid token, go off and start working with data\n    HTMLToOutput = '<html><h1>Already have token</h1></html>';\n  }\n  else {//we are starting from scratch or resetting\n    return HtmlService.createHtmlOutput(\"<html><h1>Lets start with oAuth</h1><a href='\"+getURLForAuthorization()+\"'>click here to start</a></html>\");\n  }\n  \n  HTMLToOutput += getData();\n  return HtmlService.createHtmlOutput(HTMLToOutput);\n}\n\n//see docs here - https://developers.google.com/google-apps/admin-audit/\nfunction getData(){\n  var getCustomerIdUrl = 'https://apps-apis.google.com/a/feeds/customer/2.0/customerId?alt=json';\n  var getCustomeerIdData = UrlFetchApp.fetch(getCustomerIdUrl,getUrlFetchOptions()).getContentText();  \n  \n  var responseObject = JSON.parse(getCustomeerIdData);\n  \n   //customerId is the 2nd item. make this more robust!\n  var customerId = responseObject.entry.apps$property[1].value;\n  \n  //cpanel app ID is always the same. \n  var activityUrl = 'https://www.googleapis.com/apps/reporting/audit/v1/'+customerId+'/207535951991'; \n  var dataResponse = UrlFetchApp.fetch(activityUrl,getUrlFetchOptions()).getContentText();  \n  \n  var dataObject = JSON.parse(dataResponse);\n  var ss = SpreadsheetApp.create('Audit Log');\n  for(var i = 0;i<dataObject.items.length;i++){\n    var row = [];\n    var item = dataObject.items[i];\n    if(item.actor){\n      row.push(item.actor.callerType); \n      row.push(item.actor.email);\n    }\n    for(var j=0;item.events&&j<item.events.length;j++){\n      row.push(item.events[j].eventType);\n      row.push(item.events[j].name);\n    } \n    if(item.id){\n      row.push(item.id.time);\n    }\n    row.push(item.ipAddress);\n    ss.appendRow(row);\n  }\n  SpreadsheetApp.flush();\n  return \"<a href='\"+ss.getUrl()+\"'>Open Spreadsheet here</a>\";\n}\n\n//hardcoded here for easily tweaking this. should move this to ScriptProperties or better parameterize them\nvar AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth'; //step 1. we can actually start directly here if that is necessary\nvar TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'; //step 2. after we get the callback, go get token\n\nvar CLIENT_ID = 'YOUR_CLIENT_ID';\nvar CLIENT_SECRET = 'YOUR_CLIENT_SECRET';\n\nvar REDIRECT_URL= 'YOUR_DEPLOYED_URL';//safer than ScriptApp.getService().getUrl() for domain accounts\n\n//this is the user propety where we'll store the token, make sure this is unique across all user properties across all scripts\nvar tokenPropertyName = 'GOOGLE_OAUTH_TOKEN'; \nvar baseURLPropertyName = 'GOOGLE_INSTANCE_URL'; \n\nfunction getURLForAuthorization(){\n  return AUTHORIZE_URL + '?response_type=code&client_id='+CLIENT_ID+'&redirect_uri='+REDIRECT_URL +\n    '&scope=https://apps-apis.google.com/a/feeds/policies/ https://www.googleapis.com/auth/apps/reporting/audit.readonly&state=/profile';  \n}\n\n//Google requires POST, salesforce and slc worked with GET\nfunction getAndStoreAccessToken(code){\n  var parameters = {\n    method : 'post',\n    payload : 'client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&grant_type=authorization_code&redirect_uri='+REDIRECT_URL+'&code=' + code\n  };\n  \n  var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText();   \n  var tokenResponse = JSON.parse(response);\n  \n  //store the token for later retrieval\n  UserProperties.setProperty(tokenPropertyName, tokenResponse.access_token);\n}\n\nfunction getUrlFetchOptions() {\n  var token = UserProperties.getProperty(tokenPropertyName);\n  return {\"contentType\" : \"application/json\",\n          \"headers\" : {\"Authorization\" : \"Bearer \" + token,\n                       \"Accept\" : \"application/json\"}\n         };\n}\n\n//naive check, add checking freshness or refreshing oauth tokens. also needs logout\nfunction isTokenValid() {\n  var token = UserProperties.getProperty(tokenPropertyName);\n  if(!token){ //if its empty or undefined\n    return false;\n  }\n  return true; \n}"
  },
  {
    "path": "IO2013/Drive/Code.gs",
    "content": "function doGet(e) {\n  var HTMLToOutput;\n  if(e.parameter.state){\n    var state = JSON.parse(e.parameter.state);\n    if(state.action === 'create'){\n      var newFolder =  DriveApp.createFolder('New Assignment');\n      \n      var templateQuiz = DriveApp.getFileById(QUIZ_TEMPLATE);\n      var templateRoster = DriveApp.getFileById(ROSTER_TEMPLATE);\n      \n      var newQuiz = templateQuiz.makeCopy('Quiz Questions');\n      newFolder.addFile(newQuiz);\n      DriveApp.getRootFolder().removeFile(newQuiz);\n      \n      var newRoster = templateRoster.makeCopy('Roster');\n      newFolder.addFile(newRoster);\n      DriveApp.getRootFolder().removeFile(newRoster);\n      \n      var objectToSaveForClose = { type: 'close',\n                                  rosterID : newRoster.getId()}\n      \n      var objectToSaveForPublish = { type: 'publish',\n                                    assignmentFolderID : newFolder.getId(),\n                                    rosterID : newRoster.getId(),\n                                    quizID : newQuiz.getId()}\n      \n      var closeBlob = Utilities.newBlob(JSON.stringify(objectToSaveForClose), 'application/drive-assignment-creator', 'Close Quiz');\n      var publishBlob = Utilities.newBlob(JSON.stringify(objectToSaveForPublish), 'application/drive-assignment-creator', 'Publish Quiz'); \n      newFolder.createFile(closeBlob);\n      newFolder.createFile(publishBlob);\n      \n      HTMLToOutput = 'Assignment folder created with necessary template. Please update the Roster, fill in the quiz questions and click on the Publish icon in the folder. ';\n    } else {\n      var fileID = state.ids[0];\n      var content = DriveApp.getFileById(fileID).getBlob().getDataAsString();\n      var contentObject = JSON.parse(content);\n      var ssID = contentObject.rosterID;\n        var ss = SpreadsheetApp.openById(ssID);\n        var sheet = ss.getSheets()[0];\n        var rosterRange = ss.getRangeByName('RosterRange');\n        var rosterObjects = getRowsData(sheet, rosterRange);\n        \n        \n      if(contentObject.type === 'close'){\n        for(var i in rosterObjects){\n          var rosterItem = rosterObjects[i];\n          var quickCopy = DriveApp.getFileById(rosterItem.fileid);\n          quickCopy.removeEditor(rosterItem.studentEmailAddress);\n          quickCopy.addViewer(rosterItem.studentEmailAddress); \n        }\n        HTMLToOutput = 'Quiz is now closed. Your students will no longer be able to save any changes. They wil be able to view your notes on their quizes. ';\n        \n      }else if(contentObject.type === 'publish'){\n        var quizFileID = contentObject.quizID;\n        var quiz = DriveApp.getFileById(quizFileID);\n        var folderID = contentObject.assignmentFolderID;\n        var folder = DriveApp.getFolderById(folderID);\n        \n        for(var i in rosterObjects){\n          var rosterItem = rosterObjects[i];\n          var quizCopy = quiz.makeCopy('Quiz - ' + rosterItem.studentName);\n          folder.addFile(quizCopy);\n          DriveApp.getRootFolder().removeFile(quizCopy);\n          quizCopy.addEditor(rosterItem.studentEmailAddress);\n          rosterItem.fileid = quizCopy.getId();\n        }\n        setRowsData(sheet,rosterObjects);\n        HTMLToOutput = 'Completed publishing your quizes. Your students should be able to see the files in their \"Shared with me\" view.';\n      }\n      \n    }\n    \n  }\n  else if(e.parameter.code){//if we get \"code\" as a parameter in, then this is a callback. we can make this more explicit\n    getAndStoreAccessToken(e.parameter.code);\n    HTMLToOutput = 'App is installed, you can close this window now or open up Google Drive.';\n  }\n  else {//we are starting from scratch or resetting\n  }\n  var t = HtmlService.createTemplateFromFile('ui')\n  t.message = HTMLToOutput;\n  return t.evaluate().setSandboxMode(HtmlService.SandboxMode.NATIVE)\n}\n\nfunction getURLForAuthorization(){\n  return AUTHORIZE_URL + '?response_type=code&client_id='+CLIENT_ID+'&redirect_uri='+REDIRECT_URL +'&scope=https://www.googleapis.com/auth/drive.install';  \n}\n\nfunction getAndStoreAccessToken(code){\n  var parameters = { method : 'post',\n                    payload : 'client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&grant_type=authorization_code&redirect_uri='+REDIRECT_URL+'&code=' + code};\n  //no need to do anything with the token going forward. \n  var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText();\n}\n\n\n//replace with any template files you want - \nvar ROSTER_TEMPLATE = '0AkJNj_IM2wiPdEZzbFUxMjQteGtQS1JadHg5VmFMdGc';\nvar QUIZ_TEMPLATE = '1_LIZp1lahFhNouXsZNI1s6UluBskt_LOwQGlJIj2ftY';\n\nvar AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth'; \nvar TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'; \n\n//put your URL below or use ScriptApp.getService().getUrl();\nvar REDIRECT_URL= 'https://script.google.com/macros/s/AKfycbxK6aULLp8CL47aiUM_tHZCX9-2YNOK0yrp-ujnUQV8CrUQkUGk/exec';\nvar CLIENT_ID = ScriptProperties.getProperty('CLIENT_ID')\nvar CLIENT_SECRET = ScriptProperties.getProperty('CLIENT_SECRET');\n\n"
  },
  {
    "path": "IO2013/Drive/README.md",
    "content": "Companion code for Google IO 2013 session - Integrate Google Drive with Google Apps Script\n\nhttps://developers.google.com/events/io/sessions/325412094\n\nFor setup -\n\nYou'll need to create OAuth 2 client ID/secret at http://developers.google.com/console and store it in the right ScriptProperties.\n"
  },
  {
    "path": "IO2013/Drive/SpreadsheetUtils.gs",
    "content": "//https://developers.google.com/apps-script/storing_data_spreadsheets#reading\n\n\n// getRowsData iterates row by row in the input range and returns an array of objects.\n// Each object contains all the data for a given row, indexed by its normalized column name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - columnHeadersRowIndex: specifies the row number where the column names are stored.\n//       This argument is optional and it defaults to the row immediately above range;\n// Returns an Array of objects.\nfunction getRowsData(sheet, range, columnHeadersRowIndex) {\n  columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;\n  var numColumns = range.getLastColumn() - range.getColumn() + 1;\n  var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);\n  var headers = headersRange.getValues()[0];\n  return getObjects(range.getValues(), normalizeHeaders(headers));\n}\n\n// setRowsData fills in one row of data per object defined in the objects Array.\n// For every Column, it checks if data objects define a value for it.\n// Arguments:\n//   - sheet: the Sheet Object where the data will be written\n//   - objects: an Array of Objects, each of which contains data for a row\n//   - optHeadersRange: a Range of cells where the column headers are defined. This\n//     defaults to the entire first row in sheet.\n//   - optFirstDataRowIndex: index of the first row where data should be written. This\n//     defaults to the row immediately below the headers.\nfunction setRowsData(sheet, objects, optHeadersRange, optFirstDataRowIndex) {\n  var headersRange = optHeadersRange || sheet.getRange(1, 1, 1, sheet.getMaxColumns());\n  var firstDataRowIndex = optFirstDataRowIndex || headersRange.getRowIndex() + 1;\n  var headers = normalizeHeaders(headersRange.getValues()[0]);\n\n  var data = [];\n  for (var i = 0; i < objects.length; ++i) {\n    var values = []\n    for (j = 0; j < headers.length; ++j) {\n      var header = headers[j];\n      // If the header is non-empty and the object value is 0...\n      if ((header.length > 0) && (objects[i][header] == 0)) {\n        values.push(0);\n      }\n      // If the header is empty or the object value is empty...\n      else if ((!(header.length > 0)) || (objects[i][header]=='')) {\n        values.push('');\n      }\n      else {\n        values.push(objects[i][header]);\n      }\n    }\n    data.push(values);\n  }\n\n  var destinationRange = sheet.getRange(firstDataRowIndex, headersRange.getColumnIndex(),\n                                        objects.length, headers.length);\n  destinationRange.setValues(data);\n}\n\n// getColumnsData iterates column by column in the input range and returns an array of objects.\n// Each object contains all the data for a given column, indexed by its normalized row name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - rowHeadersColumnIndex: specifies the column number where the row names are stored.\n//       This argument is optional and it defaults to the column immediately left of the range;\n// Returns an Array of objects.\nfunction getColumnsData(sheet, range, rowHeadersColumnIndex) {\n  rowHeadersColumnIndex = rowHeadersColumnIndex || range.getColumnIndex() - 1;\n  var headersTmp = sheet.getRange(range.getRow(), rowHeadersColumnIndex, range.getNumRows(), 1).getValues();\n  var headers = normalizeHeaders(arrayTranspose(headersTmp)[0]);\n  return getObjects(arrayTranspose(range.getValues()), headers);\n}\n\n\n// For every row of data in data, generates an object that contains the data. Names of\n// object fields are defined in keys.\n// Arguments:\n//   - data: JavaScript 2d array\n//   - keys: Array of Strings that define the property names for the objects to create\nfunction getObjects(data, keys) {\n  var objects = [];\n  for (var i = 0; i < data.length; ++i) {\n    var object = {};\n    var hasData = false;\n    for (var j = 0; j < data[i].length; ++j) {\n      var cellData = data[i][j];\n      if (isCellEmpty(cellData)) {\n        continue;\n      }\n      object[keys[j]] = cellData;\n      hasData = true;\n    }\n    if (hasData) {\n      objects.push(object);\n    }\n  }\n  return objects;\n}\n\n// Returns an Array of normalized Strings.\n// Arguments:\n//   - headers: Array of Strings to normalize\nfunction normalizeHeaders(headers) {\n  var keys = [];\n  for (var i = 0; i < headers.length; ++i) {\n    var key = normalizeHeader(headers[i]);\n    if (key.length > 0) {\n      keys.push(key);\n    }\n  }\n  return keys;\n}\n\n// Normalizes a string, by removing all alphanumeric characters and using mixed case\n// to separate words. The output will always start with a lower case letter.\n// This function is designed to produce JavaScript object property names.\n// Arguments:\n//   - header: string to normalize\n// Examples:\n//   \"First Name\" -> \"firstName\"\n//   \"Market Cap (millions) -> \"marketCapMillions\n//   \"1 number at the beginning is ignored\" -> \"numberAtTheBeginningIsIgnored\"\nfunction normalizeHeader(header) {\n  var key = \"\";\n  var upperCase = false;\n  for (var i = 0; i < header.length; ++i) {\n    var letter = header[i];\n    if (letter == \" \" && key.length > 0) {\n      upperCase = true;\n      continue;\n    }\n    if (!isAlnum(letter)) {\n      continue;\n    }\n    if (key.length == 0 && isDigit(letter)) {\n      continue; // first character must be a letter\n    }\n    if (upperCase) {\n      upperCase = false;\n      key += letter.toUpperCase();\n    } else {\n      key += letter.toLowerCase();\n    }\n  }\n  return key;\n}\n\n// Returns true if the cell where cellData was read from is empty.\n// Arguments:\n//   - cellData: string\nfunction isCellEmpty(cellData) {\n  return typeof(cellData) == \"string\" && cellData == \"\";\n}\n\n// Returns true if the character char is alphabetical, false otherwise.\nfunction isAlnum(char) {\n  return char >= 'A' && char <= 'Z' ||\n    char >= 'a' && char <= 'z' ||\n    isDigit(char);\n}\n\n// Returns true if the character char is a digit, false otherwise.\nfunction isDigit(char) {\n  return char >= '0' && char <= '9';\n}\n\n// Given a JavaScript 2d Array, this function returns the transposed table.\n// Arguments:\n//   - data: JavaScript 2d Array\n// Returns a JavaScript 2d Array\n// Example: arrayTranspose([[1,2,3],[4,5,6]]) returns [[1,4],[2,5],[3,6]].\nfunction arrayTranspose(data) {\n  if (data.length == 0 || data[0].length == 0) {\n    return null;\n  }\n\n  var ret = [];\n  for (var i = 0; i < data[0].length; ++i) {\n    ret.push([]);\n  }\n\n  for (var i = 0; i < data.length; ++i) {\n    for (var j = 0; j < data[i].length; ++j) {\n      ret[j][i] = data[i][j];\n    }\n  }\n\n  return ret;\n}"
  },
  {
    "path": "IO2013/Drive/ui.html",
    "content": "<!doctype html>\n<title>Google Drive Quiz Manager</title>\n\n<link rel=stylesheet\nhref=\"//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css\">\n<style>\n#throbber {\n  left: 50%;\n  height: 48px;\n  margin-left: -24px;\n  margin-top: -24px;\n  position: absolute;\n  top: 50%;\n  width: 48px;\n}\n</style>\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js\"></script>\n<script>\n$(document).ready(function() {\n  \n});\n\n\n</script>\n\n\n<legend><center><img src='http://www.google.com/images/icons/product/drive-32.png'/> Google Drive Quiz Manager <img src='http://www.google.com/images/icons/product/script-32.png'/> </center></legend>\n\n<?if(!this.message){?>\n<div id=authsetup>\n<p class=well>\n<center><h3>Please click on the link below to install the Quiz Manager app to your Google Drive. You will be redirected to a standard Google authorization page.<h3></center>\n<br><br>\n<center>\n<a href='<?=getURLForAuthorization()?>' id=authlink class=\"btn btn-primary\">Install app!</a></center>\n</p>\n</div>\n<?}else{?>\n<div id=message>\n<p class=well>\n<center><h3><?=this.message?><h3></center>\n<br><br>\n<center>\n<a href='#' id=authlink class=\"btn btn-primary\">Close window</a></center>\n</p>\n</div>\n<?}?>\n<p class=well>\n<center><img src = 'http://www.google.com/enterprise/apps/images/homepage/education_home_canvas.png'/></center>\n\n</p>\n\n\n"
  },
  {
    "path": "IO2013/YouTubeAnalytics/Code.gs",
    "content": "function onOpen(){\n  SpreadsheetApp.getActiveSpreadsheet()\n  .addMenu('YouTube Analytics',[{name:'Ad hoc report', functionName:'showUI'},\n                                {name: 'Twitter trending', functionName:'twitter'}]);\n}\n\nfunction showUI(){\n  var ui = HtmlService.createTemplateFromFile('ui').evaluate().setSandboxMode(HtmlService.SandboxMode.NATIVE).setHeight(350);\n  SpreadsheetApp.getActiveSpreadsheet().show(ui);\n}\n\nfunction sendNightlyEmail(){\n  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Published Dashboard');\n  var chart = sheet.getCharts()[0];  \n  MailApp.sendEmail(Session.getActiveUser().getEmail(), 'Daily YouTube Sharing report', 'See attached image', {name:'Sharing chart',attachments:[chart.getBlob()]});\n}\n  \nfunction alertOnViewsIncrease(){\n  //perform business logic here to compare against previously stored values/thresholds\n}\n\nfunction doGet(e) {\n  var HTMLToOutput;\n  if(e.parameter.code){//if we get \"code\" as a parameter in, then this is a callback. we can make this more explicit\n    getAndStoreAccessToken(e.parameters.code);\n    HTMLToOutput = HtmlService.createHtmlOutputFromFile('oauthsuccess').getContent();\n  }\n  else if(isTokenValid()){//we already have a valid token  but this should never start here.\n    HTMLToOutput = '<html><h1>Invalid access</h1></html>';\n  }\n  else {//we are starting from scratch but this should never start here.\n    HTMLToOutput = \"<html><h1>Invalid access</h1></html>\";\n  }\n  return HtmlService.createHtmlOutput(HTMLToOutput).setSandboxMode(HtmlService.SandboxMode.NATIVE);\n}\n\n\nfunction getData(startdate,enddate,metrics,dimensions){\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  //these are the channels and contentOwner settings we care for -  \n  var ids\t= 'contentOwner==promo-pso-brazil';\n  var filters\t= 'channel==UCEN58iXQg82TXgsDCjWqIkg';\n  \n  var sheet = ss.insertSheet();\n  ss.setActiveSheet(sheet);\n  var base_url = 'https://www.googleapis.com/youtube/analytics/v1/reports?';\n  var getDataURL = base_url + 'ids='+ids+'&start-date='+startdate+'&end-date='+enddate+'&metrics='+metrics+'&dimensions='+dimensions+'&filters='+filters;\n  Logger.log(getDataURL);\n  \n  var dataResponse = UrlFetchApp.fetch(getDataURL,getUrlFetchOptions()).getContentText();  \n  var dataObj = JSON.parse(dataResponse);\n  var headers = [];\n  for(var i = 0;dataObj.columnHeaders && i<dataObj.columnHeaders.length;i++){\n    headers.push(dataObj.columnHeaders[i].name);\n  }\n  ss.appendRow(headers);\n  for(var i = 0;dataObj.rows && i<dataObj.rows.length;i++){\n    ss.appendRow(dataObj.rows[i]);\n  }\n}\n"
  },
  {
    "path": "IO2013/YouTubeAnalytics/README.md",
    "content": "Companion code for Google IO 2013 session - Ad Hoc Analysis with Google Apps Script and YouTube Analytics API\n\nhttps://developers.google.com/events/io/sessions/328316141\n\nFor setup - \n\nYou'll need to create OAuth 2 client ID/secret at http://developers.google.com/console and store it in the right ScriptProperties.\n\nSimilarly for the Twitter sample, you'll need to create a Key/Secret with the Twitter API. \n"
  },
  {
    "path": "IO2013/YouTubeAnalytics/oauth2.gs",
    "content": "var AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth'; //step 1. we can actually start directly here if that is necessary\nvar TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'; //step 2. after we get the callback, go get token\n\nvar CLIENT_ID = ScriptProperties.getProperty('GOOGLE_YTA_OAUTH_CLIENT_ID');\nvar CLIENT_SECRET = ScriptProperties.getProperty('GOOGLE_YTA_OAUTH_CLIENT_SECRET');\n\n//PUT YOUR URL HERE -\nvar REDIRECT_URL= 'https://script.google.com/macros/s/AKfycbyFabJD1uUo3NNXAVVlFVapCRnJw7dJKBmCF3X9nzhgPmxZbRM/exec';\n\n\nvar oauthTokenPropertyName = 'GOOGLE_OAUTH_ACCESS_TOKEN'; \nvar oauthTokenExpiresPropertyName = 'GOOGLE_OAUTH_ACCESS_TOKEN_EXPIRES'; \nvar refreshTokenPropertyName = 'GOOGLE_OAUTH_REFRESH_TOKEN'; \n\n\nfunction getURLForAuthorization(){\n  return AUTHORIZE_URL + '?response_type=code&client_id='+CLIENT_ID+'&redirect_uri='+REDIRECT_URL +\n    '&scope=https://www.googleapis.com/auth/yt-analytics.readonly&approval_prompt=force&access_type=offline&state=/profile';  \n}\n\nfunction getAndStoreAccessToken(code){\n  var parameters = {\n    method : 'post',\n    payload : 'client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&grant_type=authorization_code&redirect_uri='+REDIRECT_URL+'&code=' + code\n  };\n  \n  var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText(); \n  storeOAuthValues_(response);\n}\n\nfunction getUrlFetchOptions() {\n  var token = UserProperties.getProperty(oauthTokenPropertyName);\n  return {\n    \"contentType\" : \"application/json\",\n    \"headers\" : {\n      \"Authorization\" : \"Bearer \" + token,\n      \"Accept\" : \"application/json\"\n    }\n  };\n}\n\nfunction attemptTokenRefresh_() {\n  var refreshToken = UserProperties.getProperty(refreshTokenPropertyName);\n  if (!refreshToken) {\n    Logger.log('No refresh token available to refresh with ' + refreshTokenPropertyName);\n    return false;\n  }\n  var requestData = {\n    method: 'post',\n    payload: {\n      client_id: CLIENT_ID,\n      client_secret: CLIENT_SECRET,\n      refresh_token: refreshToken,\n      grant_type: 'refresh_token'\n    }\n  };\n  Logger.log('Attempting token refresh');\n  var response = UrlFetchApp.fetch(TOKEN_URL, requestData).getContentText();\n  storeOAuthValues_(response);\n  return true;\n}\n\nfunction storeOAuthValues_(response){\n  var tokenResponse = JSON.parse(response);\n  \n  var accessToken = tokenResponse.access_token;\n  // expires_in is in seconds and Date.now is ms\n  var endMs = Date.now() + tokenResponse.expires_in * 1000;\n  var refreshToken = tokenResponse.refresh_token;\n  \n  \n  //store the token for later retrival\n  UserProperties.setProperty(oauthTokenPropertyName, accessToken);\n  if (refreshToken) { //on a refresh call we wont get a new refresh token, lets not wipe prev one out\n    UserProperties.setProperty(refreshTokenPropertyName, refreshToken);\n  }\n  UserProperties.setProperty(oauthTokenExpiresPropertyName, endMs);\n}\n\nfunction isOAuthed() {\n  if (hasValidToken_()) {\n    Logger.log('Valid oauth token found');\n    return true;\n  } else {\n    try {\n      return attemptTokenRefresh_();\n    } catch (e) {\n      Logger.log('Failed to refresh token with error: ' + e);\n      return false;\n    }\n  }\n}\n\nfunction hasValidToken_() {\n  if (!isTokenPresent_()) {\n    return false;\n  }\n  return (!isTokenExpired_());\n}\n\nfunction isTokenExpired_() {\n  var expirationTimeMs = UserProperties.getProperty(oauthTokenExpiresPropertyName);\n  if (!expirationTimeMs) {\n    return true;\n  }\n  expirationTimeMs = Number(expirationTimeMs);\n  var threshold = Date.now() + 30000;\n  return (expirationTimeMs < threshold);\n}\n\nfunction isTokenPresent_() {\n  var token = UserProperties.getProperty(oauthTokenPropertyName);\n  if(!token){ //if its empty or undefined\n    return false;\n  }\n  return true; \n}\n"
  },
  {
    "path": "IO2013/YouTubeAnalytics/oauthsuccess.html",
    "content": "<!doctype html>\n<title>YouTube Analytics</title>\n\n<link rel=stylesheet href=\"//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css\">\n\n<legend><img src='http://www.youtube.com/yt/img/logo_hh.png'/> Ad hoc Report Generator</legend>\n\n<p class=well>\nThank you for authorization the application. Please close this window and return to the spreadsheet. \n<br/></br/>\n<a href='#' id='closewindow' class=\"btn btn-primary\">Close window</a>\n</p>\n"
  },
  {
    "path": "IO2013/YouTubeAnalytics/twitter.gs",
    "content": "function twitter(){\n  var oauthCfg = UrlFetchApp.addOAuthService('twitter');\n  oauthCfg.setAccessTokenUrl('https://api.twitter.com/oauth/access_token');\n  oauthCfg.setRequestTokenUrl('https://api.twitter.com/oauth/request_token');\n  oauthCfg.setAuthorizationUrl('https://api.twitter.com/oauth/authorize');\n  oauthCfg.setConsumerKey(ScriptProperties.getProperty('TWITTER_CLIENT_ID'));\n  oauthCfg.setConsumerSecret(ScriptProperties.getProperty('TWITTER_CLIENT_SECRET'));\n  var options = {oAuthServiceName:'twitter',oAuthUseToken:'always'};\n  \n  var WOEIDs = {};\n  WOEIDs['United States'] = '23424977';\n  WOEIDs['Brazil'] = '23424768'\n  WOEIDs['South Africa'] = '23424942'\n  WOEIDs['United Kindom'] = '23424975';\n  WOEIDs['Canada'] = '23424775'\n  \n  var headers = [];\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  \n  for(var woe in WOEIDs){\n    var url = 'https://api.twitter.com/1.1/trends/place.json?id='+WOEIDs[woe]; //eg. all = 1, london = 44418\n    var response = UrlFetchApp.fetch(url, options).getContentText();\n    \n    var myObject = JSON.parse(response)[0];\n    var trends = []\n    for(var i = 0;myObject.trends && i<myObject.trends.length;i++){\n      trends.push(myObject.trends[i].name);\n    }\n    ss.appendRow([myObject.as_of,woe,trends.join(', ')]);\n  }\n  \n}\n"
  },
  {
    "path": "IO2013/YouTubeAnalytics/ui.html",
    "content": "<!doctype html>\n<title>YouTube Analytics</title>\n\n<link rel=stylesheet\nhref=\"//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css\">\n<style>\n#throbber {\n  left: 50%;\n  height: 48px;\n  margin-left: -24px;\n  margin-top: -24px;\n  position: absolute;\n  top: 50%;\n  width: 48px;\n}\n</style>\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js\"></script>\n<script>\n$(document).ready(function() {\n  $('#throbber').hide();\n  <?if (!isOAuthed()){?>\n    $('#authsetup').show(); \n    $('#configForm').hide();\n  <?} else {?>\n    $('#authsetup').hide();\n    $('#configForm').show();\n  <?}?>\n  $('#authlink').click(function(){\n    $('#authsetup').hide();\n    $('#throbber').show();\n    checkForOAuthKey();\n  });\n  $('#load').click(function(){\n    $('#configForm').hide();\n    $('#throbber').show();\n    google.script.run.withSuccessHandler(submissionSucceeded).withFailureHandler(alert).getData($('#start').val(),$('#end').val(),$('#metrics').val(),$('#dim').val());\n  });\n});\nfunction submissionSucceeded(){\n  $('#configForm').show();\n  $('#throbber').hide();\n}\n\nvar timeoutId = null;\nfunction checkForOAuthKey() {\n  //console.log(new Date() + 'checking for OAuth Key');\n  google.script.run.withSuccessHandler(gotOAuthCheckBack).isOAuthed();\n  timeoutId = window.setTimeout(checkForOAuthKey, 1500);\n}\n\nfunction gotOAuthCheckBack(e){\n  //console.log(new Date() + 'Got back ' + e + ' for oauth check');\n  if(e){\n    //console.log(new Date() + 'Clearing timeout');\n    window.clearTimeout(timeoutId);\n    timeoutId = null;\n    $('#throbber').hide();\n    $('#configForm').show();\n  }\n}\n</script>\n\n\n<img id=throbber src=\"//ssl.gstatic.com/music/ap/cec6433ac17a57a5ad3264da7b23e565/Spinner_48.gif\"\nalt=\"Loading...\">\n\n<legend><img src='http://www.youtube.com/yt/img/logo_hh.png'/> Ad hoc Report Generator</legend>\n\n\n<div id=authsetup>\n<p class=well>\nPlease click on the link below to authorize application to the YouTube Analytics API. This page will automatically refresh when you return.\n<center>\n<a href='<?=getURLForAuthorization()?>' id=authlink class=\"btn btn-primary\">Authorize app</a></center>\n</p>\n</div>\n\n\n\n<form id=configForm class=form-horizontal><fieldset>\n\n<div class=control-group>\n<label class=control-label for=start>Start date:</label>\n  <div class=controls><input id=\"start\" class=input-large name=start type=text value=\"2013-04-01\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=end>End date:</label>\n  <div class=controls><input id=\"end\" class=input-large name=end type=text value=\"2013-04-30\"></div>\n</div> \n<div class=control-group>\n  <label class=control-label for=metrics>Metrics:</label>\n  <div class=controls><select id=\"metrics\" class=input-large name=metrics>\n  <option value=\"viewerPercentage\">Viewer Percentage</option>\n  <option value=\"estimatedMinutesWatched,averageViewDuration,averageViewPercentage\">Watch time</option>\n  <option value=\"views,likes\">Engagement - Views, Likes</option>\n  <option value=\"shares\">Engagement - Shares</option>\n</select></div>\n</div> \n<div class=control-group>\n<label class=control-label for=dim>Dimensions:</label>\n<div class=controls><select id=\"dim\" class=input-large name=dim>\n  <option value=\"ageGroup,gender\">Demography</option>\n  <option value=\"day\">Day</option>\n  <option value=\"country\">Country</option>\n  <option value=\"sharingService\">Sharing service</option>\n</select></div>\n</div> \n\n<div class=form-actions> \n<button id=load class=\"btn btn-primary\" type=submit>Load</button>\n<button id=cancel class=\"btn btn-info\" type=button>Cancel</button>\n</div>\n</fieldset></form>\n"
  },
  {
    "path": "NYTimesCampaignContribution.gs",
    "content": "function getData(){\n  var API_KEY = 'YOUR_API_KEY_HERE';\n  var url = 'http://api.nytimes.com/svc/elections/us/v3/finances/2012/president/states/NY.json?api-key='+API_KEY;\n  \n  var response = UrlFetchApp.fetch(url).getContentText();\n  var respObj = JSON.parse(response);\n  \n  var ss = SpreadsheetApp.create('NY Presidential Campaign')\n  //add a simple header\n  ss.appendRow(['Candidate','Number of Contributions','Total $ Contributions']);\n  \n  for(var i in respObj.results){\n    ss.appendRow([respObj.results[i].full_name,respObj.results[i].contribution_count,respObj.results[i].total]);\n  }\n}"
  },
  {
    "path": "README.md",
    "content": "GoogleAppsScript\n================\n\nSample code for Google Apps Script. \nThese are not official Google samples. These are more of proof of concepts to support demos and other presentations. \n\nPlease use this code as a guidline and not as production ready code. \n\nhttp://developers.google.com/apps-script\n\nhttp://script.google.com\n\nAll code in this folder released under [Apache 2.0 license](http://www.apache.org/licenses/LICENSE-2.0). \n"
  },
  {
    "path": "SAP/Forms/Code.gs",
    "content": "function onFormSubmit(e) {\n  var myObject = {};\n  var itemResponses = e.response.getItemResponses();\n   for (var j = 0; j < itemResponses.length; j++) {\n     var itemResponse = itemResponses[j];\n     myObject[itemResponse.getItem().getTitle()] = itemResponse.getResponse();\n     //Logger.log('\"%s\" was \"%s\"',itemResponse.getItem().getTitle(), itemResponse.getResponse());\n   }\n  \n  Logger.log(myObject);\n  \n   var t = HtmlService.createTemplateFromFile('product_template');\n  t.data = myObject;\n  var postPayload =  t.evaluate().getContent();\n  \n  Logger.log(postPayload);\n  \n  var base_url = \"https://sapes1.sapdevcenter.com/sap/opu/odata/IWBEP/GWDEMO/ProductCollection\";\n  var additionParams = \"$format=json\";\n  var user = 'YOUR_USERNAME';\n  var password = 'YOUR_PASSWORD';\n  var header = 'Basic '+Utilities.base64Encode(user+':'+password);\n  \n  var headers = {\n    Authorization: header,\n    'X-CSRF-Token' : 'Fetch',\n     \"Content-Type\": \"application/atom+xml\"\n  }\n                                        \n                                        \n  //Logger.log(headers);\n  var response = UrlFetchApp.fetch(base_url,{headers: headers, muteHttpExceptions: true});\n  var csrf_token = response.getAllHeaders()['x-csrf-token'];\n  var cookie = response.getAllHeaders()['set-cookie'];\n  \n  //Logger.log(response.getAllHeaders());\n  \n  headers['X-CSRF-Token'] = csrf_token;\n  headers['Cookie'] = cookie.join('; ');\n  \n  \n  \n  Logger.log(headers);\n  \n  //var deleteResponse = UrlFetchApp.fetch(id,{headers: headers, method : 'DELETE', muteHttpExceptions: true});\n  \n  \n  var createResponse = UrlFetchApp.fetch(base_url,{headers: headers, method : 'POST', contentType : \"application/atom+xml\", payload: postPayload, muteHttpExceptions: true});\n  \n  Logger.log(createResponse.getContentText());\n}\n"
  },
  {
    "path": "SAP/Forms/product_template.html",
    "content": "<atom:entry xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" xml:base=\"https://sapes1.sapdevcenter.com:443/sap/opu/odata/IWBEP/GWDEMO/\">\n<atom:content type=\"application/xml\">\n <m:properties>\n<d:ProductName><?=data['Product Name']?></d:ProductName>\n<d:TaxTariffText>Regular VAT</d:TaxTariffText>\n<d:ProductID>ARUN-<?=Math.random().toString(36).substring(2, 4)?></d:ProductID>\n<d:DimensionUnit>MTR</d:DimensionUnit>\n<d:UnitPrice>500.00</d:UnitPrice>\n<d:WeightMeasure>4.200</d:WeightMeasure>\n<d:ProductWidth>0.320</d:ProductWidth>\n<d:SupplierID>0100000000</d:SupplierID>\n<d:ProductDescription><?=data['Product Description']?></d:ProductDescription>\n<d:ProductCategory>Notebooks</d:ProductCategory>\n<d:TaxTariffCode>1</d:TaxTariffCode>\n<d:QuantityUnit>EA</d:QuantityUnit>\n<d:ProductTypeCode>PR</d:ProductTypeCode>\n<d:WeightUnitCode>KGM</d:WeightUnitCode>\n<d:SupplierName>SAP</d:SupplierName>\n<d:CurrencyCode>EUR</d:CurrencyCode>\n<d:ProductHeight>0.040</d:ProductHeight>\n<d:ProductDepth>0.210</d:ProductDepth>\n</m:properties>\n</atom:content>\n</atom:entry>"
  },
  {
    "path": "SAP/Gmail Schemas/Code.gs",
    "content": "function testSchemas() {\n  var htmlBody = HtmlService.createHtmlOutputFromFile('mail_template').getContent();\n\n  GmailApp.sendEmail(Session.getEffectiveUser().getEmail(),'Alert for SAP - ' + new Date(),'',\n    {htmlBody: htmlBody});\n}\n"
  },
  {
    "path": "SAP/Gmail Schemas/mail_template.html",
    "content": "<html>\n  <head>\n    <script type=\"application/ld+json\">\n{\n  \"@context\": \"http://schema.org\",\n  \"@type\": \"ParcelDelivery\",\n  \"deliveryAddress\": {\n    \"@type\": \"PostalAddress\",\n    \"streetAddress\": \"24 Willie Mays Plaza\",\n    \"addressLocality\": \"San Francisco\",\n    \"addressRegion\": \"CA\",\n    \"addressCountry\": \"US\",\n    \"postalCode\": \"94107\"\n  },\n  \"expectedArrival\": \"2013-03-12T12:00:00-08:00\",\n  \"carrier\": {\n    \"@type\": \"Organization\",\n    \"name\": \"SAP\"\n  },\n  \"itemShipped\": {\n    \"@type\": \"Product\",\n    \"name\": \"iPod Mini\"\n  },\n  \"partOfOrder\": {\n    \"@type\": \"Order\",\n    \"orderNumber\": \"176057\",\n    \"seller\": {\n      \"@type\": \"Person\",\n      \"name\": \"Bob Dole\"\n    }\n  },\n  \"action\": {\n    \"@type\": \"TrackAction\",\n    \"name\": \"Track Order in SAP\",\n    \"url\": \"https://www.package-shipments.com/track?package=abc\"\n  }\n}\n</script>\n  </head>\n  <body>\n    <p>\n      This a test for a Go-To action in Gmail.\n    </p>\n  </body>\n</html>"
  },
  {
    "path": "SAP/README.md",
    "content": "SAP Demos\n================\n\nSample SAP integrations. You'll need to create a [sample SAP account](http://scn.sap.com/docs/DOC-40986) at the SCN.\n\nYou can watch the video of the [sample demos here](http://www.slideshare.net/SAPAppsPartner/tehcnical-webinar-technical-webinar-building-mashups-with-google-apps-and-sap-using-sap-netweaver-gateway) (start at about 1 hr in for Google Apps demos)\n\nThese are not official Google samples. These are more of proof of concepts to support demos and other presentations. \n\nPlease use this code as a guidline and not as production ready code. \n\nhttp://developers.google.com/apps-script\n\nhttp://script.google.com\n\nAll code in this folder released under [Apache 2.0 license](http://www.apache.org/licenses/LICENSE-2.0). \n"
  },
  {
    "path": "SAP/Sheets/Code.gs",
    "content": "//sample instance\n//http://scn.sap.com/docs/DOC-40986\n\n\nfunction onOpen(){\n  SpreadsheetApp.getActiveSpreadsheet()\n  .addMenu('SAP NetWeaver',[{name:'SAP Data Wizard', functionName:'showUI'},\n                            null,\n                            {name:'Load Business Partners', functionName:'getBusinessPartnerData'},\n                            {name:'Load Sales Orders', functionName:'getSalesOrderData'},\n                            null,\n                            {name:'Load Items for Sales Order', functionName:'loadSalesItems'},\n                            null,\n                            {name: 'Post to Google+', functionName:'googleplus'}]);//future ideas\n}\n\nfunction showUI(){\n  var ui = HtmlService.createTemplateFromFile('ui').evaluate().setSandboxMode(HtmlService.SandboxMode.NATIVE).setHeight(550);\n  \n  SpreadsheetApp.getActiveSpreadsheet().show(ui);\n}\n\nfunction returnDate(unix_timestamp){\n  //var unix_timestamp = '1370025951';\n  var date = new Date(unix_timestamp*1000);\n  //Logger.log(date);\n  return date;\n}\n\nfunction returnUnixtimestamp(dateString){\n  //var dateString = '2013-04-01';\n  var parts = dateString.match(/(\\d+)/g);  \n  var date = new Date(parts[0], parts[1]-1, parts[2]); // months are 0-based\n  var timestamp = date.getTime()/1000 + '';\n  return timestamp;\n}\n\n\nfunction createOrSetActiveSheet(sheetName){\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  var sheets = ss.getSheets();\n  var sheet;\n  for(var i = 0;i<sheets.length;i++){\n    sheet = sheets[i];\n    var currentSheetName = sheet.getSheetName();\n    if(sheetName===currentSheetName){\n      ss.setActiveSheet(sheet);\n      sheet.clear();\n      return sheet;\n    }\n  }\n  sheet = ss.insertSheet();\n  sheet.setName(sheetName);\n  ss.setActiveSheet(sheet);\n  return sheet;\n}\n\n//future ideas\nfunction sendNightlyEmail(){\n  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Published Dashboard');\n  var chart = sheet.getCharts()[0];  \n  MailApp.sendEmail(Session.getActiveUser().getEmail(), 'Daily petition report', 'See attached image', {name:'Sharing chart',attachments:[chart.getBlob()]});\n}\n\n\nfunction getBusinessPartnerData(){\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  var sheet = ss.getActiveSheet();\n  sheet.clear();\n  \n  \n  var base_url = \"https://sapes1.sapdevcenter.com/sap/opu/odata/IWBEP/GWDEMO/BusinessPartnerCollection/?\";\n  var additionParams = \"$format=json\";\n  var user = 'YOUR_USERNAME';\n  var password = 'YOUR_PASSWORD';\n  var header = 'Basic '+Utilities.base64Encode(user+':'+password);\n  \n  \n  var getDataURL = base_url + additionParams;\n  Logger.log(getDataURL);\n  \n  var dataResponse = UrlFetchApp.fetch(getDataURL,{headers: {Authorization: header}, muteHttpExceptions: true}).getContentText();  \n  var dataObj = JSON.parse(dataResponse);\n  var headers = ['Key','Name','Role','Website','Email','Address'];\n  \n  var maxLetter = String.fromCharCode(64 + headers.length);\n  \n  sheet.appendRow(headers);\n  \n  sheet.getRange('A1:'+maxLetter+'1').setBackground('Grey').setFontStyle('oblique');\n  \n  ss.setColumnWidth(2, 220);\n  ss.setColumnWidth(3, 89);\n  \n  var rows = [];\n  for(var i = 0;dataObj.d.results && i<dataObj.d.results.length;i++){\n    var row = dataObj.d.results[i];\n    \n    rows.push([row.BusinessPartnerKey,row.Company,row.BusinessPartnerRoleText,row.WebAddress, row.EmailAddress, 'street address']);\n  }\n  var rowsCount = rows.length+1;\n  ss.getRange('A2:'+maxLetter+rowsCount).setValues(rows);\n}\n\n\nfunction getSalesOrderData(){\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  var sheet = ss.getActiveSheet();\n  sheet.clear();\n  \n  \n  var base_url = \"https://sapes1.sapdevcenter.com/sap/opu/odata/IWBEP/GWDEMO/SalesOrderCollection/?\";\n  var additionParams = \"$format=json\";\n  var user = 'YOUR_USERNAME';\n  var password = 'YOUR_PASSWORD';\n  var header = 'Basic '+Utilities.base64Encode(user+':'+password);\n  \n  \n  var getDataURL = base_url + additionParams;\n  Logger.log(getDataURL);\n  \n  var dataResponse = UrlFetchApp.fetch(getDataURL,{headers: {Authorization: header}, muteHttpExceptions: true}).getContentText();  \n  var dataObj = JSON.parse(dataResponse);\n  var headers = ['Sales Order Key','Customer Name','Status','Currency','Total Amt','Tax Amt','Note','Created By','Changed By'];\n  \n  var maxLetter = String.fromCharCode(64 + headers.length);\n  \n  sheet.appendRow(headers);\n  \n  sheet.getRange('A1:'+maxLetter+'1').setBackground('Grey').setFontStyle('oblique');\n  \n  ss.setColumnWidth(2, 220);\n  ss.setColumnWidth(3, 89);\n  \n  var rows = [];\n  for(var i = 0;dataObj.d.results && i<dataObj.d.results.length;i++){\n    var row = dataObj.d.results[i];\n    \n    rows.push([row.SalesOrderKey,row.CustomerName,row.StatusDescription,row.Currency,row.TotalSum,row.Tax,row.Note,row.CreatedByEmployeeLastName,row.ChangedByEmployeeLastName]);\n  }\n  var rowsCount = rows.length+1;\n  ss.getRange('A2:'+maxLetter+rowsCount).setValues(rows);\n}\n\n\nfunction loadSalesItems(){\n var salesOrderKey = SpreadsheetApp.getActiveRange().getValue();\n  var initialPageCount = 99; //because a default sheet has only 100 rows to start\n  \n  var sheet = createOrSetActiveSheet(salesOrderKey);\n  var base_url = \"https://sapes1.sapdevcenter.com/sap/opu/odata/IWBEP/GWDEMO/SalesOrderCollection('\"+salesOrderKey+\"')/salesorderlineitems?\";\n  var getDataURL = base_url + '$format=json';\n  \n  var user = 'YOUR_USERNAME';\n  var password = 'YOUR_PASSWORD';\n  var header = 'Basic '+Utilities.base64Encode(user+':'+password);\n  Logger.log(getDataURL);\n\n  var dataResponse = UrlFetchApp.fetch(getDataURL,{headers: {Authorization: header}, muteHttpExceptions: true}).getContentText();  \n  Logger.log(dataResponse);\n  var dataObj = JSON.parse(dataResponse);\n  var headers = ['SalesOrderItemKey','ProductName','Availability','Note','Currency','NetSum','Tax','TotalSum'];\n  var maxLetter = String.fromCharCode(64 + headers.length);\n  \n  sheet.appendRow(headers);\n  sheet.getRange('A1:'+maxLetter+'1').setBackground('Grey').setFontStyle('oblique');\n  var rows = [];\n  for(var i = 0;dataObj.d.results && i<dataObj.d.results.length;i++){\n    var row = dataObj.d.results[i];\n    rows.push([row.SalesOrderItemKey,row.ProductName,row.Availability,row.Note,row.Currency,row.NetSum,row.Tax,row.TotalSum]);\n  }\n  var rowsCount = rows.length+1;\n  sheet.getRange('A2:'+maxLetter+rowsCount).setValues(rows);\n}"
  },
  {
    "path": "SAP/Sheets/ui.html",
    "content": "<!--\nThis is an incomplete place holder for just showing the UI\n\n-->\n\n<link rel=stylesheet\nhref=\"//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css\">\n<style>\n#throbber {\n  left: 50%;\n  height: 48px;\n  margin-left: -24px;\n  margin-top: -24px;\n  position: absolute;\n  top: 50%;\n  width: 48px;\n}\n</style>\n\n<img id=throbber src=\"//ssl.gstatic.com/music/ap/cec6433ac17a57a5ad3264da7b23e565/Spinner_48.gif\"\nalt=\"Loading...\">\n\n<legend><img src='http://www.sap.com/global/ui/images/global/sap-logo.png'/>SAP NetWeaver</legend>\n\n<form id=configForm class=form-horizontal><fieldset>\n<div class=control-group>\n<label class=control-label for=title_keyword>Title keyword:</label>\n  <div class=controls><input id=\"title_keyword\" class=input-large name=title_keyword type=text value=\"\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=body_keyword>Body Keyword:</label>\n  <div class=controls><input id=\"body_keyword\" class=input-large name=body_keyword type=text value=\"\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=start>Start date:</label>\n  <div class=controls><input id=\"start\" class=input-large name=start type=date value=\"2013-04-01\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=end>End date:</label>\n  <div class=controls><input id=\"end\" class=input-large name=end type=date value=\"2013-04-30\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=sig_floor>Minumum Signatures:</label>\n  <div class=controls><input id=\"sig_floor\" class=input-large name=sig_floor type=text value=\"0\"></div>\n</div> \n<div class=control-group>\n  <label class=control-label for=metrics>Priority:</label>\n  <div class=controls><select id=\"issues\" class=input-large name=issues>\n  <option value=\"\"></option>\n  <option value=\"1\">High</option>\n  <option value=\"2\">Medium</option>\n  <option value=\"3\">Low</option>\n</select></div>\n</div>\n<div class=control-group>\n<label class=control-label for=dim>Status:</label>\n<div class=controls><select id=\"status\" class=input-large name=status>\n  <option value=\"\"></option>\n  <option value=\"open\">Open</option>\n  <option value=\"closed\">Closed</option>\n  <option value=\"pending response\">Pending Response</option>\n  <option value=\"responded\">Responded</option>\n</select></div>\n</div> \n\n<div class=form-actions> \n<button id=load class=\"btn btn-primary\" type=submit>Load</button>\n<button id=cancel class=\"btn btn-info\" type=button onclick='google.script.host.close()'>Close</button>\n</div>\n</fieldset></form>\n\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js\"></script>\n<script>\n$(document).ready(function() {\n  $('#throbber').hide();\n\n  $('#load').click(function(){\n    $('#configForm').hide();\n    $('#throbber').show();\n    google.script.run.withSuccessHandler(submissionSucceeded).withFailureHandler(alert).getData($('#title_keyword').val(),\n                                                                                                $('#body_keyword').val(),\n                                                                                                $('#start').val(),\n                                                                                                $('#end').val(),\n                                                                                                $('#sig_floor').val(),\n                                                                                                $('#status').val());\n  });\n});\nfunction submissionSucceeded(){\n  $('#configForm').show();\n  $('#throbber').hide();\n}\n\n\n</script>\n"
  },
  {
    "path": "Salesforce.com/OAuthAndUploadContactsToSalesforce.gs",
    "content": "function onInstall(){\n  onOpen();\n}\n\nfunction onOpen() {\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  var menuEntries = [ {name: \"Upload to SalesForce\", functionName: \"salesforceEntryPoint\"}];\n  ss.addMenu(\"Salesforce.com\", menuEntries);\n}\n\nfunction salesforceEntryPoint(){\n  if(isTokenValid()){\n    HTMLToOutput = '<html><h1>Already have token</h1></html>';\n    //HTMLToOutput += getData();\n    HTMLToOutput += uploadData();\n  }\n  else {//we are starting from scratch or resetting\n    HTMLToOutput = \"<html><h1>You need to login</h1><a href='\"+getURLForAuthorization()+\"'>click here to start</a><br>Re-open this window when you return.</html>\";\n  }\n  SpreadsheetApp.getActiveSpreadsheet().show(HtmlService.createHtmlOutput(HTMLToOutput));\n}\n\nfunction doGet(e) {\n  var HTMLToOutput;\n  if(e.parameters.code){//if we get \"code\" as a parameter in, then this is a callback. we can make this more explicit\n    getAndStoreAccessToken(e.parameters.code);\n    HTMLToOutput = '<html><h1>Finished with oAuth</h1>You can close this window.</html>';\n  }\n  return HtmlService.createHtmlOutput(HTMLToOutput);\n}\n\n//do meaningful salesforce access here\nfunction getData(){\n  return runSOQL('SELECT+name+from+Account');\n}\n\nfunction runSOQL(soql){\n  var getDataURL = UserProperties.getProperty(baseURLPropertyName) + '/services/data/v26.0/query/?q='+soql;\n  var dataResponse = UrlFetchApp.fetch(getDataURL,getUrlFetchOptions()).getContentText();  \n  return dataResponse;\n}\n\nfunction uploadData() {\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  var sheet = ss.getSheets()[0];\n  var contactDataRange = ss.getDataRange();\n  var contactObjects = getRowsData(sheet, contactDataRange,1);\n  var runningLog = '<br>Uploaded following:<br><br>';\n  for(var i = 1;i<contactObjects.length;i++){\n    var payload =  Utilities.jsonStringify(\n      {\"FirstName\" : contactObjects[i].firstname,\n       \"LastName\" : contactObjects[i].lastname,\n       \"Email\" : contactObjects[i].email,\n       \"Phone\" : contactObjects[i].phone\n      }\n    );\n    Logger.log('trying ' + payload);\n    var getDataURL = UserProperties.getProperty(baseURLPropertyName) + '/services/data/v26.0/sobjects/Contact/';\n    runningLog += UrlFetchApp.fetch(getDataURL,getUrlFetchPOSTOptions(payload)).getContentText() + '<br>';  \n  }\n  return runningLog;\n}\n\n////oAuth related code\n\n//hardcoded here for easily tweaking this. should move this to ScriptProperties or better parameterize them\n//step 1. we can actually start directly here if that is necessary\nvar AUTHORIZE_URL = 'https://login.salesforce.com/services/oauth2/authorize'; \n//step 2. after we get the callback, go get token\nvar TOKEN_URL = 'https://login.salesforce.com/services/oauth2/token'; \n\n//PUT YOUR OWN SETTINGS HERE\nvar CLIENT_ID = '3MVG9y6x0357HlefXsMa7Fg0tYH0SLtBZwLWyhSP4hBGExVuUjoWLJ8rfZ1jyyw0ZRQt7H28rByHHJjRQlqMs';\nvar CLIENT_SECRET='SECRET_HERE';\nvar REDIRECT_URL= ScriptApp.getService().getUrl();\n\n//this is the user propety where we'll store the token, make sure this is unique across all user properties across all scripts\nvar tokenPropertyName = 'SALESFORCE_OAUTH_TOKEN'; \nvar baseURLPropertyName = 'SALESFORCE_INSTANCE_URL'; \n\n\n//this is the URL where they'll authorize with salesforce.com\n//may need to add a \"scope\" param here. like &scope=full for salesforce\nfunction getURLForAuthorization(){\n  return AUTHORIZE_URL + '?response_type=code&client_id='+CLIENT_ID+'&redirect_uri='+REDIRECT_URL\n}\n\nfunction getAndStoreAccessToken(code){\n  var nextURL = TOKEN_URL + '?client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&grant_type=authorization_code&redirect_uri='+REDIRECT_URL+'&code=' + code;\n  \n  var response = UrlFetchApp.fetch(nextURL).getContentText();   \n  var tokenResponse = JSON.parse(response);\n  \n  //salesforce requires you to call against the instance URL that is against the token (eg. https://na9.salesforce.com/)\n  UserProperties.setProperty(baseURLPropertyName, tokenResponse.instance_url);\n  //store the token for later retrival\n  UserProperties.setProperty(tokenPropertyName, tokenResponse.access_token);\n}\n\n\nfunction getUrlFetchOptions() {\n  var token = UserProperties.getProperty(tokenPropertyName);\n  return {\n    \"contentType\" : \"application/json\",\n    \"headers\" : {\n      \"Authorization\" : \"Bearer \" + token,\n      \"Accept\" : \"application/json\"\n    }\n  };\n}\n\n\n\nfunction getUrlFetchPOSTOptions(payload){\n  var token = UserProperties.getProperty(tokenPropertyName);\n  return {\n    \"method\": \"post\",\n    \"contentType\" : \"application/json\",\n    \"payload\" : payload,\n    \"headers\" : {\n      \"Authorization\" : \"Bearer \" + token\n    }\n  }\n}\n\nfunction isTokenValid() {\n  var token = UserProperties.getProperty(tokenPropertyName);\n  if(!token){ //if its empty or undefined\n    return false;\n  }\n  return true; //naive check\n}"
  },
  {
    "path": "Salesforce.com/RowUtilities.gs",
    "content": "// getRowsData iterates row by row in the input range and returns an array of objects.\n// Each object contains all the data for a given row, indexed by its normalized column name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - columnHeadersRowIndex: specifies the row number where the column names are stored.\n//       This argument is optional and it defaults to the row immediately above range;\n// Returns an Array of objects.\nfunction getRowsData(sheet, range, columnHeadersRowIndex) {\n  columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;\n  var numColumns = range.getEndColumn() - range.getColumn() + 1;\n  var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);\n  var headers = headersRange.getValues()[0];\n  return getObjects(range.getValues(), normalizeHeaders(headers));\n}\n\n// getColumnsData iterates column by column in the input range and returns an array of objects.\n// Each object contains all the data for a given column, indexed by its normalized row name.\n// Arguments:\n//   - sheet: the sheet object that contains the data to be processed\n//   - range: the exact range of cells where the data is stored\n//   - rowHeadersColumnIndex: specifies the column number where the row names are stored.\n//       This argument is optional and it defaults to the column immediately left of the range;\n// Returns an Array of objects.\nfunction getColumnsData(sheet, range, rowHeadersColumnIndex) {\n  rowHeadersColumnIndex = rowHeadersColumnIndex || range.getColumnIndex() - 1;\n  var headersTmp = sheet.getRange(range.getRow(), rowHeadersColumnIndex, range.getNumRows(), 1).getValues();\n  var headers = normalizeHeaders(arrayTranspose(headersTmp)[0]);\n  return getObjects(arrayTranspose(range.getValues()), headers);\n}\n\n\n// For every row of data in data, generates an object that contains the data. Names of\n// object fields are defined in keys.\n// Arguments:\n//   - data: JavaScript 2d array\n//   - keys: Array of Strings that define the property names for the objects to create\nfunction getObjects(data, keys) {\n  var objects = [];\n  for (var i = 0; i < data.length; ++i) {\n    var object = {};\n    var hasData = false;\n    for (var j = 0; j < data[i].length; ++j) {\n      var cellData = data[i][j];\n      if (isCellEmpty(cellData)) {\n        continue;\n      }\n      object[keys[j]] = cellData;\n      hasData = true;\n    }\n    if (hasData) {\n      objects.push(object);\n    }\n  }\n  return objects;\n}\n\n// Returns an Array of normalized Strings.\n// Arguments:\n//   - headers: Array of Strings to normalize\nfunction normalizeHeaders(headers) {\n  var keys = [];\n  for (var i = 0; i < headers.length; ++i) {\n    var key = normalizeHeader(headers[i]);\n    if (key.length > 0) {\n      keys.push(key);\n    }\n  }\n  return keys;\n}\n\n// Normalizes a string, by removing all alphanumeric characters and using mixed case\n// to separate words. The output will always start with a lower case letter.\n// This function is designed to produce JavaScript object property names.\n// Arguments:\n//   - header: string to normalize\n// Examples:\n//   \"First Name\" -> \"firstName\"\n//   \"Market Cap (millions) -> \"marketCapMillions\n//   \"1 number at the beginning is ignored\" -> \"numberAtTheBeginningIsIgnored\"\nfunction normalizeHeader(header) {\n  var key = \"\";\n  var upperCase = false;\n  for (var i = 0; i < header.length; ++i) {\n    var letter = header[i];\n    if (letter == \" \" && key.length > 0) {\n      upperCase = true;\n      continue;\n    }\n    if (!isAlnum(letter)) {\n      continue;\n    }\n    if (key.length == 0 && isDigit(letter)) {\n      continue; // first character must be a letter\n    }\n    if (upperCase) {\n      upperCase = false;\n      key += letter.toUpperCase();\n    } else {\n      key += letter.toLowerCase();\n    }\n  }\n  return key;\n}\n\n// Returns true if the cell where cellData was read from is empty.\n// Arguments:\n//   - cellData: string\nfunction isCellEmpty(cellData) {\n  return typeof(cellData) == \"string\" && cellData == \"\";\n}\n\n// Returns true if the character char is alphabetical, false otherwise.\nfunction isAlnum(char) {\n  return char >= 'A' && char <= 'Z' ||\n    char >= 'a' && char <= 'z' ||\n    isDigit(char);\n}\n\n// Returns true if the character char is a digit, false otherwise.\nfunction isDigit(char) {\n  return char >= '0' && char <= '9';\n}\n\n// Given a JavaScript 2d Array, this function returns the transposed table.\n// Arguments:\n//   - data: JavaScript 2d Array\n// Returns a JavaScript 2d Array\n// Example: arrayTranspose([[1,2,3],[4,5,6]]) returns [[1,4],[2,5],[3,6]].\nfunction arrayTranspose(data) {\n  if (data.length == 0 || data[0].length == 0) {\n    return null;\n  }\n\n  var ret = [];\n  for (var i = 0; i < data[0].length; ++i) {\n    ret.push([]);\n  }\n\n  for (var i = 0; i < data.length; ++i) {\n    for (var j = 0; j < data[i].length; ++j) {\n      ret[j][i] = data[i][j];\n    }\n  }\n\n  return ret;\n}\n"
  },
  {
    "path": "Salesforce.com/ScanEmailToSalesforce.gs",
    "content": "\nfunction scanEmail(){\n  //for performance sake, we'll scan top 3 threads for now. \n  //a more intelligent trigger would know it last left off and then pick it up from there\n  var threads = GmailApp.getInboxThreads(0, 5);\n  for (var i = 0; i < threads.length; i++) {\n    var message = threads[i].getMessages()[0]; //for now, only scan first message\n    if(message.getAttachments().length < 1)\n      continue;\n    \n\n    var attachment = message.getAttachments()[0]; //for now, only first attachment\n    Logger.log(attachment.getName());\n    var base64content = Utilities.base64Encode(attachment.getAs('application/pdf').getBytes());\n    \n    \n    var fromAddress = message.getFrom();\n    //this will return of the the format - Arun Nagarajan <arun.appsscript@gmail.com>\n    var actualAddress = /<(.*?)>/.exec(fromAddress)[1];\n    var soql = encodeURIComponent(\"select Id from contact where Email='\"+actualAddress+\"'\");\n    //Logger.log(actualAddress);\n    var responseObject = Utilities.jsonParse(runSOQL(soql));\n    if(responseObject.totalSize !== 0){\n      var contactId = responseObject.records[0].Id;\n      \n      var payload = Utilities.jsonStringify(\n        {\n          'Name' : attachment.getName() + '_GmailAttachment.pdf',  \n          'ParentId' : contactId,\n          'body' : base64content\n        });\n      Logger.log(contactId);\n      var getDataURL = UserProperties.getProperty(baseURLPropertyName) + '/services/data/v26.0/sobjects/Attachment/';\n      var response = UrlFetchApp.fetch(getDataURL,getUrlFetchPOSTOptions(payload)).getContentText();\n      Logger.log(response);\n    }\n  }\n}\n"
  },
  {
    "path": "Salesforce.com/readme.md",
    "content": "Click the image below for video with more details - \n\n[![ScreenShot](https://i.ytimg.com/vi/9SEAmNDtlcA/hqdefault.jpg)](http://www.youtube.com/watch?v=9SEAmNDtlcA&list=PL68F511F6E3C122EB)"
  },
  {
    "path": "ScriptDbVisualizer/Code.gs",
    "content": "var db = ScriptDb.getMyDb();\n\nfunction doGet() {\n  return HtmlService.createHtmlOutputFromFile('ScriptDbConsole');\n}\n\nfunction saveObjToDb(obj){\n  try{\n    var savedObj = db.save(obj);\n    return \"Saved ID - \" + savedObj.getId();\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction getCount(query){\n  try{\n    return \"Current count for query is - \" + db.count(query);\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction loadIDsFromDb(idlist){\n  try{\n    return db.load(idlist);\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction deleteByIds(idlist){\n  var obs_to_remove = loadIDsFromDb(idlist);\n  var results = db.removeBatch(obs_to_remove, false);\n  if (db.allOk(results)) {\n    return \"Delete by IDs successfull!\";\n  }\n  var failedObs = [];\n  for (var i = 0; i < results.length; i++) {\n    if (!results[i].successful()) {\n      failedObs.push(obs_to_remove[i]);\n    }\n  }\n  return \"Failed to delete \" + failedObs.length + \" item(s) out of \" + results.length;\n}\n\nfunction queryFromDb(query){\n  try{\n    var result = db.query(query);\n    var response = {};\n    while (result.hasNext()) {\n      var current = result.next();\n      response[current.getId()] = current;\n    }\n    return response;\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction deleteAll(query) {\n  try{\n    while (true) {\n      var result = db.query(query); \n      if (result.getSize() == 0) {\n        break;\n      }\n      while (result.hasNext()) {\n        db.remove(result.next());\n      }\n    }\n    return \"Delete for specified query successful!\";\n  }catch(e){\n    log(e);\n    throw(e);\n  }\n}\n\nfunction log(msg){\n  //write to a logger here\n  //DocsList.getFileById('0B0JNj_IM2wiPMS1lZTFhZjVjNC0yZTBjLTRiOWItYWVhMy0yYTU1ZjdkMGVkMGE').append(msg+'\\n');\n}"
  },
  {
    "path": "ScriptDbVisualizer/ScriptDbConsole.html",
    "content": "<html>\n  <style>\n    pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; }\n    .string { color: green; }\n    .number { color: darkorange; }\n    .boolean { color: blue; }\n    .null { color: magenta; }\n    .key { color: red; }\n  </style>\n  <script src=\"http://code.jquery.com/jquery-1.7.2.min.js\"></script>\n  <script>\n    function output(inp) {\n      $('#results').prepend(inp);\n    }\n    //courtesy of http://stackoverflow.com/a/7220510/662551\n    function syntaxHighlight(json) {\n      json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n      return json.replace(/(\"(\\\\u[a-zA-Z0-9]{4}|\\\\[^u]|[^\\\\\"])*\"(\\s*:)?|\\b(true|false|null)\\b|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?)/g, function (match) {\n        var cls = 'number';\n        if (/^\"/.test(match)) {\n          if (/:$/.test(match)) {\n            cls = 'key';\n          } else {\n            cls = 'string';\n          }\n        } else if (/true|false/.test(match)) {\n          cls = 'boolean';\n        } else if (/null/.test(match)) {\n          cls = 'null';\n        }\n        return '<span class=\"' + cls + '\">' + match + '</span>';\n      });\n    }\n    \n    $(document).ready(function() {\n      $('#saveItem').click(function(){\n        var input = $('#textarea').val();\n        input = JSON.parse(input);\n        google.script.run.withSuccessHandler(handleMessageResponse).withFailureHandler(addToLog).saveObjToDb(input);\n      });\n      $('#runQuery').click(function(){\n        var input = $('#textarea').val();\n        input = JSON.parse(input);\n        google.script.run.withSuccessHandler(handleQueryResponse).withFailureHandler(addToLog).queryFromDb(input);\n      });\n      $('#deleteAll').click(function(){\n        var input = $('#textarea').val();\n        input = JSON.parse(input);\n        google.script.run.withSuccessHandler(handleMessageResponse).withFailureHandler(addToLog).deleteAll(input);\n      });\n      $('#loadId').click(function(){\n        var input = $('#textarea').val();\n        var re = /\\s*,\\s*/;\n        var idList = input.split(re);\n        google.script.run.withSuccessHandler(handleQueryResponse).withFailureHandler(addToLog).loadIDsFromDb(idList);\n      });\n      $('#deleteByIds').click(function(){\n        var input = $('#textarea').val();\n        var re = /\\s*,\\s*/;\n        var idList = input.split(re);\n        google.script.run.withSuccessHandler(handleMessageResponse).withFailureHandler(addToLog).deleteByIds(idList);\n      });\n      $('#count').click(function(){\n        var input = $('#textarea').val();\n        input = JSON.parse(input);\n        google.script.run.withSuccessHandler(handleMessageResponse).withFailureHandler(addToLog).getCount(input);\n      });\n    });\n    function handleQueryResponse(responseObj){\n      $('#results').prepend('<hr><hr>');\n      if(Object.keys(responseObj).length==0){\n        output(\"No results found for query!\");\n      }\n      //TODO: for load by IDs, this will print index, not object IDs\n      for (var prop in responseObj) {\n        if (responseObj.hasOwnProperty(prop)) {\n          output('<pre>ID: ' + prop+'\\n'+syntaxHighlight(JSON.stringify(responseObj[prop], undefined, 4))+'</pre>');\n        }\n      }\n    }\n    function handleMessageResponse(response){\n      $('#results').prepend('<hr><hr>');\n      output(response); \n    }\n    function addToLog(error){\n      $('#results').prepend('<hr><hr>');\n      output(\"Error \" + JSON.stringify(error)); \n    }\n  </script>\n  <body>\n    <h2>Simple ScriptDb tester</h2>\n    \n    <b>Enter a JavaScript Object to store, a ScriptDb query to run or IDs to lookup and press the appropriate button below</b><br>\n    <textarea id=\"textarea\" rows=\"10\" cols=\"100\"></textarea><br>\n    <button id=\"count\">Get Count</button>\n    <button id=\"saveItem\">Save Object</button>\n    <button id=\"runQuery\">Run Query</button>\n    <button id=\"loadId\">Load by ID</button>\n    <button id=\"deleteByIds\">Delete by ID</button>\n    <button id=\"deleteAll\">Delete for Query</button>\n    \n    <br><br>\n    <div id=\"results\"></div>\n    <br><br>\n    By <a href=\"https://plus.google.com/117678608428606781684/posts\">Arun Nagarajan</a>\n  </body>\n</html>\n"
  },
  {
    "path": "ScriptDbVisualizer/readme.md",
    "content": "Click the image below for video with more details - \n\n[![ScreenShot](https://i.ytimg.com/vi/aShR3R8--wU/hqdefault.jpg)](http://www.youtube.com/watch?v=aShR3R8--wU&list=PL68F511F6E3C122EB)\n"
  },
  {
    "path": "Twilio/MakePhoneCall/MakePhoneCall.gs",
    "content": "var ACCOUNT_SID = 'ACd4e8e6872e581bf4cf560d37fb9059db';\nvar ACCOUNT_TOKEN = 'YOUR_TOKEN_HERE';\n\n//This is a spreadsheet bound script that is also deployed as a web app\n\n//Have a menu or a button point this function to kick things off\nfunction readRows() {\n  var sheet = SpreadsheetApp.getActiveSheet();\n  var rows = sheet.getDataRange();\n  var numRows = rows.getNumRows();\n  var values = rows.getValues();\n  \n  //for testing purposes just make call against first row\n  //expecting columns in this order - name, number, message - \n  for (var i = 1; i <= 1/*numRows - 1*/; i++) {\n    makePhoneCall(values[i][0],values[i][1],values[i][2]);\n  }\n}\n\nfunction makePhoneCall(name,number, message){\n  //URL is the callback to the current service with the message\n  var url = ScriptApp.getService().getUrl() + '?MSG'+message.replace(/ /g,'+'); //can't seem to send = in here\n  Logger.log(url);\n  var payload = {\n    \"From\" : \"2246773902\"\n    ,\"To\" : number\n    ,\"Url\": url\n    ,\"Method\" : \"GET\"\n  };\n  \n  var headers = {\n    \"Authorization\" : \"Basic \" + Utilities.base64Encode(ACCOUNT_SID + ':' + ACCOUNT_TOKEN)\n  };\n  \n  // Because payload is a JavaScript object, it will be interpreted as \n  // an HTML form. (We do not need to specify contentType; it will\n  // automatically default to either 'application/x-www-form-urlencoded' \n  // or 'multipart/form-data')\n  \n  var options =\n      {\n        \"method\" : \"post\",\n        \"payload\" : payload,\n        \"headers\" : headers\n      };\n  Logger.log(\"calling \" + name + \" at \" + number + \" with messsage - \" + message);           \n  var url = 'https://api.twilio.com/2010-04-01/Accounts/'+ACCOUNT_SID+'/Calls.json';\n  var response = UrlFetchApp.fetch(url, options);\n  //Logger.log(response.getResponseCode());\n  //Logger.log(response.getContentText());\n}\n\n//entry point to the call back\nfunction doGet(args){\n  var msg = '';\n  for (var p in args.parameters) {\n    //there are many incoming query params. lets find ours starting with a MSG\n    if(p.indexOf('MSG') > -1){\n      msg += p.replace('MSG','');\n      break;\n    }\n    // there HAS to tbe a better way to get the entire query string!\n  }\n  \n  var t = HtmlService.createTemplateFromFile(\"twiml.html\");\n  t.msg = msg;\n  var content = t.evaluate().getContent();\n  return ContentService.createTextOutput(content).setMimeType(ContentService.MimeType.XML);\n}\n\n\n\n"
  },
  {
    "path": "Twilio/MakePhoneCall/twiml.html",
    "content": "<Response>\n     <Say voice=\"woman\" language=\"en-us\"><?=msg?></Say>\n     <Play loop=\"1\">https://api.twilio.com/cowbell.mp3</Play>     \n</Response>"
  },
  {
    "path": "Twilio/RecieveSMS/RecieveSMS.gs",
    "content": "//This is deployed as a web app and in the Twilio interface you'll point incoming SMS to the deployed URL\n//deploy it to run as you and allow anyone (even anonymous) can access it\n\nfunction doGet(args) {\n  var spreadsheetID = 'YOUR_SPREAD_SHEETID';\n  \n  //Incoming params are documented here - https://www.twilio.com/docs/api/twiml/sms/twilio_request\n  var vote = args.parameter.Body;\n  var from = args.parameter.From;\n  \n  //sample poll here for favorite football team in NYC\n  var actualVote;\n  switch (vote.toLowerCase()) {\n   case \"a\":\n      actualVote = 'Giants';\n      break;\n   case \"b\":\n      actualVote = 'Jets';\n      break;\n   default:\n      actualVote = 'Dont care';\n  }\n  \n  SpreadsheetApp.openById(spreadsheetID).appendRow([from,actualVote,vote]);\n  \n  return ContentService.createTextOutput('').setMimeType(ContentService.MimeType.TEXT);\n  //empty message sends no response. save a penny!\n  \n  //other funn stuff - quickly translate\n  //return ContentService.createTextOutput(LanguageApp.translate(args.parameter.Body, \"en\", \"es\")).setMimeType(ContentService.MimeType.TEXT);\n};\n\n\n"
  },
  {
    "path": "Twilio/readme.md",
    "content": "Click the image below for video with more details - \n\n[![ScreenShot](https://i.ytimg.com/vi/j0wjM1Ds3lc/hqdefault.jpg)](http://www.youtube.com/watch?v=j0wjM1Ds3lc&list=PL68F511F6E3C122EB)\n"
  },
  {
    "path": "WhiteHouseHackday/Code.gs",
    "content": "function onOpen(){\n  SpreadsheetApp.getActiveSpreadsheet()\n  .addMenu('We the people',[{name:'Search Petitions', functionName:'showUI'},\n                            {name:'Load Signatures', functionName:'loadSignatures'},\n                            null,\n                            {name: 'Twitter trending', functionName:'twitter'}]);//future ideas\n}\n\nfunction showUI(){\n  var ui = HtmlService.createTemplateFromFile('ui').evaluate().setSandboxMode(HtmlService.SandboxMode.NATIVE).setHeight(550);\n  \n  SpreadsheetApp.getActiveSpreadsheet().show(ui);\n}\n\nfunction returnDate(unix_timestamp){\n  //var unix_timestamp = '1370025951';\n  var date = new Date(unix_timestamp*1000);\n  //Logger.log(date);\n  return date;\n}\n\nfunction returnUnixtimestamp(dateString){\n  //var dateString = '2013-04-01';\n  var parts = dateString.match(/(\\d+)/g);  \n  var date = new Date(parts[0], parts[1]-1, parts[2]); // months are 0-based\n  var timestamp = date.getTime()/1000 + '';\n  return timestamp;\n}\n\n\nfunction createOrSetActiveSheet(sheetName){\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  var sheets = ss.getSheets();\n  var sheet;\n  for(var i = 0;i<sheets.length;i++){\n    sheet = sheets[i];\n    var currentSheetName = sheet.getSheetName();\n    if(sheetName===currentSheetName){\n      ss.setActiveSheet(sheet);\n      sheet.clear();\n      return sheet;\n    }\n  }\n  sheet = ss.insertSheet();\n  sheet.setName(sheetName);\n  ss.setActiveSheet(sheet);\n  return sheet;\n}\n\n//future ideas\nfunction sendNightlyEmail(){\n  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Published Dashboard');\n  var chart = sheet.getCharts()[0];  \n  MailApp.sendEmail(Session.getActiveUser().getEmail(), 'Daily petition report', 'See attached image', {name:'Sharing chart',attachments:[chart.getBlob()]});\n}\n\n/*\nid: \"51a8efdfa9a0b1c82c000006\",\ntype: \"petition\",\ntitle: \"Condemn the actions taken against peaceful protesters in Istanbul Turkey.\",\nbody: \"There are gross human rights violations occurring in Istanbul, Turkey. Police have fired water cannons and smoke bombs into peaceful protests against the plans to put a development in the park Gezi in Taksim. http://www.bbc.co.uk/news/world-europe-22732139 http://www.nytimes.com/2013/06/01/world/europe/police-attack-protesters-in-istanbuls-taksim-square.html?pagewanted=all&_r=0 Signing this petition would help draw international attention to the issue. \",\nissues: [\n{\nid: \"20\",\nname: \"Environment\"\n},\n{\nid: \"28\",\nname: \"Human Rights\"\n},\n{\nid: \"175\",\nname: \"Urban Policy\"\n}\n],\nsignatureThreshold: 100000,\nsignatureCount: 2012,\nsignaturesNeeded: 97988,\nurl: \"https://petitions.whitehouse.gov/petition/condemn-actions-taken-against-peaceful-protesters-istanbul-turkey/zDGtGCDZ\",\ndeadline: 1372617951,\nstatus: \"open\",\nresponse: null,\ncreated: 1370025951\n*/\n\nfunction getData(title,body,startdate,enddate,sig,status){\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  var sheet = ss.getActiveSheet();\n  sheet.clear();\n  var base_url = 'https://api.whitehouse.gov/v1/petitions.json?';\n  \n  var additionParams = Utilities.formatString('title=%s&body=%s&signatureCountFloor=%s&status=%s&createdAfter=%s&createdBefore=%s'\n                                              ,title,body,sig,status,returnUnixtimestamp(startdate),returnUnixtimestamp(enddate));\n  \n  var getDataURL = base_url + 'limit=99&offset=0&'+additionParams;\n  Logger.log(getDataURL);\n  var dataResponse = UrlFetchApp.fetch(getDataURL).getContentText();  \n  var dataObj = JSON.parse(dataResponse);\n  var headers = ['ID','Title','Body','Deadline','Status','Response','Created','Signature Count'];\n  var maxLetter = String.fromCharCode(64 + headers.length);\n  \n  sheet.appendRow(headers);\n  \n  sheet.getRange('A1:'+maxLetter+'1').setBackground('Grey').setFontStyle('oblique');\n  \n  ss.setColumnWidth(2, 220);\n  ss.setColumnWidth(3, 625);\n  var rows = [];\n  for(var i = 0;dataObj.results && i<dataObj.results.length;i++){\n    var row = dataObj.results[i];\n    var responseFormula = \"\";\n    if (row.response) {\n      responseFormula = row.response.url;\n    }\n    rows.push(['=hyperlink(\"'+row.url+'\",\"'+row.id+'\")',row.title,row.body,returnDate(row.deadline),row.status,responseFormula,returnDate(row.created),row.signatureCount]);\n  }\n  var rowsCount = rows.length+1;\n  ss.getRange('A2:'+maxLetter+rowsCount).setValues(rows);\n}\n\n\n/*\n{\nid: \"50d43ea16ce61c5910000008\",\ntype: \"signature\",\nname: \"D. S.\",\ncity: \"Bethel\",\nstate: \"VT\",\nzip: \"05032\",\ncreated: 1356086945\n}\n*/\nfunction loadSignatures(){\n var petitionId = SpreadsheetApp.getActiveRange().getValue();\n  var initialPageCount = 99; //because a default sheet has only 100 rows to start\n  \n  var sheet = createOrSetActiveSheet(petitionId);\n  var base_url = 'https://api.whitehouse.gov/v1/petitions/'+petitionId+'/signatures.json?';\n  var getDataURL = base_url + 'limit='+initialPageCount+'&offset=0';\n\n  var dataResponse = UrlFetchApp.fetch(getDataURL).getContentText();  \n  var dataObj = JSON.parse(dataResponse);\n  var headers = ['ID','Name','City','State','ZIP','Created'];\n  var maxLetter = String.fromCharCode(64 + headers.length);\n  \n  sheet.appendRow(headers);\n  sheet.getRange('A1:'+maxLetter+'1').setBackground('Grey').setFontStyle('oblique');\n  var rows = [];\n  for(var i = 0;dataObj.results && i<dataObj.results.length;i++){\n    var row = dataObj.results[i];\n    rows.push([row.id,row.name,row.city,row.state,row.zip,returnDate(row.created)]);\n  }\n  var rowsCount = rows.length+1;\n  sheet.getRange('A2:'+maxLetter+rowsCount).setValues(rows);\n}"
  },
  {
    "path": "WhiteHouseHackday/README.md",
    "content": "Title: Google Spreadsheet Integration\n\nDevelopers: Arun Nagarajan from Google and Daniel McLaughlin from the Boston Globe\n\nDescription: Easily search and process petition data directly through Google Spreadsheets. This tool could be a framework to exploring other open APIs within an environment familiar to many researchers and journalists.\n\nCode is very early and cleanup/refactoring needed ;)\n\n![Screenshot of \"We the Spreadsheet\"](https://f.cloud.github.com/assets/979487/595221/47b9c01c-caeb-11e2-9095-b67156071732.png\n)\n\n"
  },
  {
    "path": "WhiteHouseHackday/ui.html",
    "content": "<!doctype html>\n<title>We the People</title>\n\n<link rel=stylesheet\nhref=\"//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css\">\n<style>\n#throbber {\n  left: 50%;\n  height: 48px;\n  margin-left: -24px;\n  margin-top: -24px;\n  position: absolute;\n  top: 50%;\n  width: 48px;\n}\n</style>\n<script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js\"></script>\n<script>\n$(document).ready(function() {\n  $('#throbber').hide();\n\n  $('#load').click(function(){\n    $('#configForm').hide();\n    $('#throbber').show();\n    google.script.run.withSuccessHandler(submissionSucceeded).withFailureHandler(alert).getData($('#title_keyword').val(),\n                                                                                                $('#body_keyword').val(),\n                                                                                                $('#start').val(),\n                                                                                                $('#end').val(),\n                                                                                                $('#sig_floor').val(),\n                                                                                                $('#status').val());\n  });\n});\nfunction submissionSucceeded(){\n  $('#configForm').show();\n  $('#throbber').hide();\n}\n\n\n</script>\n\n\n<img id=throbber src=\"//ssl.gstatic.com/music/ap/cec6433ac17a57a5ad3264da7b23e565/Spinner_48.gif\"\nalt=\"Loading...\">\n\n<legend><img src='https://petitions.whitehouse.gov/profiles/petitions/themes/fortyfour/images/featured_petitions.jpg'/> Search Petitions</legend>\n\n<form id=configForm class=form-horizontal><fieldset>\n<div class=control-group>\n<label class=control-label for=title_keyword>Title keyword:</label>\n  <div class=controls><input id=\"title_keyword\" class=input-large name=title_keyword type=text value=\"\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=body_keyword>Body Keyword:</label>\n  <div class=controls><input id=\"body_keyword\" class=input-large name=body_keyword type=text value=\"\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=start>Start date:</label>\n  <div class=controls><input id=\"start\" class=input-large name=start type=date value=\"2013-04-01\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=end>End date:</label>\n  <div class=controls><input id=\"end\" class=input-large name=end type=date value=\"2013-04-30\"></div>\n</div> \n<div class=control-group>\n<label class=control-label for=sig_floor>Minumum Signatures:</label>\n  <div class=controls><input id=\"sig_floor\" class=input-large name=sig_floor type=text value=\"0\"></div>\n</div> \n<div class=control-group>\n  <label class=control-label for=metrics>Issue:</label>\n  <div class=controls><select id=\"issues\" class=input-large name=issues>\n  <option value=\"\"></option>\n  <option value=\"1\">Agriculture</option>\n  <option value=\"2\">Arts and Humanities</option>\n  <option value=\"3\">Budget and Taxes</option>\n  <option value=\"4\">Civil Rights and Liberties</option>\n  <option value=\"8\">Climate Change</option>\n  <option value=\"9\">Consumer Protections</option>\n  <option value=\"10\">Criminal Justice and Law Enforcement</option>\n  <option value=\"12\">Defense</option>\n  <option value=\"13\">Disabilities</option>\n  <option value=\"16\">Economy</option>\n  <option value=\"18\">Education</option>\n  <option value=\"19\">Energy</option>\n  <option value=\"20\">Environment</option>\n  <option value=\"21\">Family</option>\n  <option value=\"22\">Firearms</option>\n  <option value=\"24\">Foreign Policy</option>\n  <option value=\"25\">Health Care</option>\n  <option value=\"26\">Homeland Security and Disaster Relief</option>\n  <option value=\"27\">Housing</option>\n  <option value=\"28\">Human Rights</option>\n  <option value=\"29\">Immigration</option>\n  <option value=\"30\">Innovation</option>\n  <option value=\"97\">Job Creation</option>\n  <option value=\"103\">Labor</option>\n  <option value=\"109\">Natural Resources</option>\n  <option value=\"115\">Postal Service</option>\n  <option value=\"121\">Poverty</option>\n  <option value=\"127\">Regulatory Reform</option>\n  <option value=\"133\">Rural Policy</option>\n  <option value=\"139\">Science and Space Policy</option>\n  <option value=\"145\">Social Security</option>\n  <option value=\"151\">Small Business</option>\n  <option value=\"157\">Technology and Telecommunications</option>\n  <option value=\"163\">Trade</option>\n  <option value=\"169\">Transportation and Infrastructure</option>\n  <option value=\"175\">Urban Policy</option>\n  <option value=\"181\">Veterans and Military Families</option>\n  <option value=\"187\">Women's Issues</option>\n  <option value=\"193\">Government Reform</option>\n</select></div>\n</div>\n<div class=control-group>\n<label class=control-label for=dim>Status:</label>\n<div class=controls><select id=\"status\" class=input-large name=status>\n  <option value=\"\"></option>\n  <option value=\"open\">Open</option>\n  <option value=\"closed\">Closed</option>\n  <option value=\"pending response\">Pending Response</option>\n  <option value=\"responded\">Responded</option>\n</select></div>\n</div> \n\n<div class=form-actions> \n<button id=load class=\"btn btn-primary\" type=submit>Load</button>\n<button id=cancel class=\"btn btn-info\" type=button onclick='google.script.host.close()'>Close</button>\n</div>\n</fieldset></form>\n"
  },
  {
    "path": "YouTube/CreateGDLCalendar.gs",
    "content": "function createCalendar(){\n  var API_KEY = 'YOU_API_KEY_HERE';\n  var cal = CalendarApp.createCalendar('GDL Calendar');\n  var url = 'https://www.googleapis.com/youtube/v3/activities?'\n              +'part=snippet&channelId=UC_x5XG1OV2P6uZZ5FSM9Ttw&maxResults=20&publishedBefore=2013-02-25T00:00:00.0Z'\n              +'&key='+API_KEY;\n  var response = UrlFetchApp.fetch(url).getContentText();\n  \n  var responseObject = JSON.parse(response);\n  \n  for(var i=0;responseObject.items && i<responseObject.items.length;i++){\n    var item = responseObject.items[i];\n    cal.createEvent(item.snippet.title, new Date(item.snippet.publishedAt), new Date(item.snippet.publishedAt));\n  }\n  \n  Logger.log('https://www.google.com/calendar/embed?src='+cal.getId());\n}"
  },
  {
    "path": "YouTube/YouTubeAnalytics.gs",
    "content": "/**\nThis is a spreadsheet bound script. \n4 menus will be added. The first two show case one demo and the second two the other. \n**/\n\n//Import in Library ID = MGwgKN2Th03tJ5OdmlzB8KPxhMjh3Sh48\nvar _ = Underscore.load();\nvar API_KEY = 'YOU_API_KEY';\n\nfunction onOpen() {\n  SpreadsheetApp.getActive().addMenu('YouTube', [\n    {name: 'Sync With Playlist', functionName: 'sync'},\n    {name: 'Update Views', functionName: 'update'},\n    {name: 'Do OAuth', functionName: 'getAuth'},\n    {name: 'Load Analytics', functionName: 'getData'}\n  ]);\n}\n\n//Original Author - Eric Koleda\nfunction sync() {\n  var ss = SpreadsheetApp.getActive();\n  var sheet = ss.getActiveSheet();\n  var existing = {};\n  var data = sheet.getDataRange().getValues();\n  var PLAYLIST_ID = Browser.inputBox('Please provide a playlist ID', 'Enter playlist ID here without quotes, eg - PLBC4C67EE5D2B46FB or EC3EF8DA2DEBF26B67', Browser.Buttons.OK);\n  \n  _.each(data, function(row) {\n    existing[row[0]] = row;\n  });\n  var url = _.sprintf('https://www.googleapis.com/youtube/v3/playlistItems?key=%s&part=snippet&playlistId=%s&maxResults=50',\n                      API_KEY, PLAYLIST_ID);\n  var result = JSON.parse(UrlFetchApp.fetch(url).getContentText());\n  _.each(result.items, function(item) {\n    var id = item.snippet.resourceId.videoId;\n    if (!existing[id]) {\n      sheet.appendRow([id, new Date(item.snippet.publishedAt), item.snippet.title, 'http://www.youtube.com/watch?v=' + id])\n    }\n  });\n  sheet.sort(2);\n}\n\nfunction update() {\n  var ss = SpreadsheetApp.getActive();\n  var sheet = ss.getActiveSheet();\n  var views = [];\n  var data = sheet.getDataRange().getValues();\n  var ids = _.map(data, function(row) {\n    return row[0];\n  });\n  var url = _.sprintf('https://www.googleapis.com/youtube/v3/videos?key=%s&part=statistics&id=%s',\n                      API_KEY, ids.join(','));\n  var result = JSON.parse(UrlFetchApp.fetch(url).getContentText());\n  var viewCounts = {};\n  _.each(result.items, function(item) {\n    var id = item.id;\n    viewCounts[id] = item.statistics.viewCount;\n  });\n  var views = [];\n  _.each(data, function(row) {\n    views.push([viewCounts[row[0]]]);\n  });\n  sheet.getRange(1, 5, views.length).setValues(views);\n}\n\n\nfunction getAuth() {\n  var HTMLToOutput = '';\n  if(isTokenValid()){//if we already have a valid token, go off and start working with data\n    HTMLToOutput = '<html><h1>Already have token</h1></html>';\n  }\n  else {//we are starting from scratch or resetting\n    HTMLToOutput = \"<html><h1>Lets start with oAuth</h1><a href='\"+getURLForAuthorization()+\"'>click here to start</a></html>\";\n  }\n  \n  SpreadsheetApp.getActiveSpreadsheet().show(HtmlService.createHtmlOutput(HTMLToOutput));\n}\n\nfunction doGet(e) {\n  var HTMLToOutput;\n  if(e.parameters.code){//if we get \"code\" as a parameter in, then this is a callback. we can make this more explicit\n    getAndStoreAccessToken(e.parameters.code);\n    HTMLToOutput = '<html><h1>Finished with oAuth. You can close the window now</h1></html>';\n  }\n  \n  else {//we are starting from scratch or resetting\n    return HtmlService.createHtmlOutput(\"<html><h1>Lets start with oAuth</h1><a href='\"+getURLForAuthorization()+\"'>click here to start</a></html>\");\n  }\n  \n  return HtmlService.createHtmlOutput(HTMLToOutput);\n}\n\n/**\nThis assumes the first 6 rows of the active sheet is these two columns. \nids - channel==UC_x5XG1OV2P6uZZ5FSM9Ttw\nstart-date - 2013-01-01\nend-date - 2013-02-25\nmetrics - estimatedMinutesWatched\ndimensions - 30DayTotals\nfilters - country==US\n**/\nfunction getData(){\n  var ss = SpreadsheetApp.getActiveSpreadsheet();\n  \n  var ids\t= ss.getActiveSheet().getRange(1, 2).getValue();\n  var startdate\t= ss.getActiveSheet().getRange(2, 2).getValue();\n  var enddate\t= ss.getActiveSheet().getRange(3, 2).getValue();\n  var metrics\t= ss.getActiveSheet().getRange(4, 2).getValue();\n  var dimensions = ss.getActiveSheet().getRange(5, 2).getValue();\n  var filters\t= ss.getActiveSheet().getRange(6, 2).getValue();\n  \n  var base_url = 'https://www.googleapis.com/youtube/analytics/v1/reports?key='+API_KEY;\n  var getDataURL = base_url + '&ids='+ids+'&start-date='+startdate+'&end-date='+enddate+'&metrics='+metrics+'&dimensions='+dimensions+'&filters='+filters;\n  Logger.log(getDataURL);\n  \n  var dataResponse = UrlFetchApp.fetch(getDataURL,getUrlFetchOptions()).getContentText();  \n  var dataObj = JSON.parse(dataResponse);\n  var headers = [];\n  for(var i = 0;dataObj.columnHeaders && i<dataObj.columnHeaders.length;i++){\n    headers.push(dataObj.columnHeaders[i].name);\n  }\n  ss.appendRow(headers);\n  for(var i = 0;dataObj.rows && i<dataObj.rows.length;i++){\n    ss.appendRow(dataObj.rows[i]);\n  }\n  Logger.log(dataResponse)\n  return dataResponse;\n}\n\n\nvar AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/auth'; //step 1. we can actually start directly here if that is necessary\nvar TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'; //step 2. after we get the callback, go get token\n\nvar CLIENT_ID = 'YOUR_CLIENT_ID';\nvar CLIENT_SECRET = 'YOUR_CLIENT_SECRET';\n\nvar REDIRECT_URL= ScriptApp.getService().getUrl();\nvar tokenPropertyName = 'GOOGLE_OAUTH_TOKEN'; \nvar baseURLPropertyName = 'GOOGLE_INSTANCE_URL'; \n\n\n//this is the URL where they'll authorize with salesforce.com\n//may need to add a \"scope\" param here. like &scope=full for salesforce\n//example scope for google - https://www.googleapis.com/plus/v1/activities\nfunction getURLForAuthorization(){\n  return AUTHORIZE_URL + '?response_type=code&client_id='+CLIENT_ID+'&redirect_uri='+REDIRECT_URL +\n    '&scope=https://www.googleapis.com/auth/yt-analytics.readonly&state=/profile';  \n}\n\nfunction getAndStoreAccessToken(code){\n  var parameters = {\n    method : 'post',\n    payload : 'client_id='+CLIENT_ID+'&client_secret='+CLIENT_SECRET+'&grant_type=authorization_code&redirect_uri='+REDIRECT_URL+'&code=' + code\n  };\n  \n  var response = UrlFetchApp.fetch(TOKEN_URL,parameters).getContentText();   \n  var tokenResponse = JSON.parse(response);\n  \n  //store the token for later retrival\n  UserProperties.setProperty(tokenPropertyName, tokenResponse.access_token);\n}\n\nfunction getUrlFetchOptions() {\n  var token = UserProperties.getProperty(tokenPropertyName);\n  return {\n    \"contentType\" : \"application/json\",\n    \"headers\" : {\n      \"Authorization\" : \"Bearer \" + token,\n      \"Accept\" : \"application/json\"\n    }\n  };\n}\n\n// we don't have a logout option here. for now, manually clear out the token under File->Project->User Properties\nfunction isTokenValid() {\n  var token = UserProperties.getProperty(tokenPropertyName);\n  if(!token){ //if its empty or undefined\n    return false;\n  }\n  return true; //naive check\n  \n  //if your API has a more fancy token checking mechanism, use it. for now we just check to see if there is a token. \n  /*\n  var responseString;\n  try{\n  responseString = UrlFetchApp.fetch(BASE_URI+'/api/rest/system/session/check',getUrlFetchOptions(token)).getContentText();\n  }catch(e){ //presumably an HTTP 401 will go here\n  return false;\n  }\n  if(responseString){\n  var responseObject = JSON.parse(responseString);\n  return responseObject.authenticated;\n  }\n  return false;*/\n}"
  },
  {
    "path": "YouTube/readme.md",
    "content": "Click the image below for video with more details - \n\n[![ScreenShot](https://i.ytimg.com/vi/VVhsK5jH6u8/hqdefault.jpg)](http://www.youtube.com/watch?v=VVhsK5jH6u8&list=PL68F511F6E3C122EB)"
  }
]