[
  {
    "path": ".gitignore",
    "content": ".crashes\n.gradle\n/local.properties\n/.idea/\n.DS_Store\n/build\n*.iml\n\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"submodules/airshare\"]\n\tpath = submodules/airshare\n\turl = https://github.com/OnlyInAmerica/AirShare-Android.git\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: android\nandroid:\n  update-sdk: true\n  components:\n    - platform-tools\n    - build-tools-22.0.1\n    - android-22\n    - extra-android-m2repository\n    - sys-img-armeabi-v7a-android-22\n\n#before_script:\n#  - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a\n#  - emulator -avd test -no-skin -no-audio -no-window &\n#  - bash ./android-wait-for-emulator.sh\n#  - adb shell input keyevent 82 &\n\nscript:\n  - ./gradlew assembleDebug\n  # Currently only builds, does not run tests.\n  # Enable testing with './gradlew connectedCheck'\n  # once I figure out this emulator business\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. \"Contributor\"\n\n     means each individual or legal entity that creates, contributes to the\n     creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n\n     means the combination of the Contributions of others (if any) used by a\n     Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n\n     means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n\n     means Source Code Form to which the initial Contributor has attached the\n     notice in Exhibit A, the Executable Form of such Source Code Form, and\n     Modifications of such Source Code Form, in each case including portions\n     thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n     means\n\n     a. that the initial Contributor has attached the notice described in\n        Exhibit B to the Covered Software; or\n\n     b. that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the terms of\n        a Secondary License.\n\n1.6. \"Executable Form\"\n\n     means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n\n     means a work that combines Covered Software with other material, in a\n     separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n\n     means this document.\n\n1.9. \"Licensable\"\n\n     means having the right to grant, to the maximum extent possible, whether\n     at the time of the initial grant or subsequently, any and all of the\n     rights conveyed by this License.\n\n1.10. \"Modifications\"\n\n     means any of the following:\n\n     a. any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered Software; or\n\n     b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. \"Patent Claims\" of a Contributor\n\n      means any patent claim(s), including without limitation, method,\n      process, and apparatus claims, in any patent Licensable by such\n      Contributor that would be infringed, but for the grant of the License,\n      by the making, using, selling, offering for sale, having made, import,\n      or transfer of either its Contributions or its Contributor Version.\n\n1.12. \"Secondary License\"\n\n      means either the GNU General Public License, Version 2.0, the GNU Lesser\n      General Public License, Version 2.1, the GNU Affero General Public\n      License, Version 3.0, or any later versions of those licenses.\n\n1.13. \"Source Code Form\"\n\n      means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n\n      means an individual or a legal entity exercising rights under this\n      License. For legal entities, \"You\" includes any entity that controls, is\n      controlled by, or is under common control with You. For purposes of this\n      definition, \"control\" means (a) the power, direct or indirect, to cause\n      the direction or management of such entity, whether by contract or\n      otherwise, or (b) ownership of more than fifty percent (50%) of the\n      outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n     Each Contributor hereby grants You a world-wide, royalty-free,\n     non-exclusive license:\n\n     a. under intellectual property rights (other than patent or trademark)\n        Licensable by such Contributor to use, reproduce, make available,\n        modify, display, perform, distribute, and otherwise exploit its\n        Contributions, either on an unmodified basis, with Modifications, or\n        as part of a Larger Work; and\n\n     b. under Patent Claims of such Contributor to make, use, sell, offer for\n        sale, have made, import, and otherwise transfer either its\n        Contributions or its Contributor Version.\n\n2.2. Effective Date\n\n     The licenses granted in Section 2.1 with respect to any Contribution\n     become effective for each Contribution on the date the Contributor first\n     distributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\n     The licenses granted in this Section 2 are the only rights granted under\n     this License. No additional rights or licenses will be implied from the\n     distribution or licensing of Covered Software under this License.\n     Notwithstanding Section 2.1(b) above, no patent license is granted by a\n     Contributor:\n\n     a. for any code that a Contributor has removed from Covered Software; or\n\n     b. for infringements caused by: (i) Your and any other third party's\n        modifications of Covered Software, or (ii) the combination of its\n        Contributions with other software (except as part of its Contributor\n        Version); or\n\n     c. under Patent Claims infringed by Covered Software in the absence of\n        its Contributions.\n\n     This License does not grant any rights in the trademarks, service marks,\n     or logos of any Contributor (except as may be necessary to comply with\n     the notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n     No Contributor makes additional grants as a result of Your choice to\n     distribute the Covered Software under a subsequent version of this\n     License (see Section 10.2) or under the terms of a Secondary License (if\n     permitted under the terms of Section 3.3).\n\n2.5. Representation\n\n     Each Contributor represents that the Contributor believes its\n     Contributions are its original creation(s) or it has sufficient rights to\n     grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n     This License is not intended to limit any rights You have under\n     applicable copyright doctrines of fair use, fair dealing, or other\n     equivalents.\n\n2.7. Conditions\n\n     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n     Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n     All distribution of Covered Software in Source Code Form, including any\n     Modifications that You create or to which You contribute, must be under\n     the terms of this License. You must inform recipients that the Source\n     Code Form of the Covered Software is governed by the terms of this\n     License, and how they can obtain a copy of this License. You may not\n     attempt to alter or restrict the recipients' rights in the Source Code\n     Form.\n\n3.2. Distribution of Executable Form\n\n     If You distribute Covered Software in Executable Form then:\n\n     a. such Covered Software must also be made available in Source Code Form,\n        as described in Section 3.1, and You must inform recipients of the\n        Executable Form how they can obtain a copy of such Source Code Form by\n        reasonable means in a timely manner, at a charge no more than the cost\n        of distribution to the recipient; and\n\n     b. You may distribute such Executable Form under the terms of this\n        License, or sublicense it under different terms, provided that the\n        license for the Executable Form does not attempt to limit or alter the\n        recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n     You may create and distribute a Larger Work under terms of Your choice,\n     provided that You also comply with the requirements of this License for\n     the Covered Software. If the Larger Work is a combination of Covered\n     Software with a work governed by one or more Secondary Licenses, and the\n     Covered Software is not Incompatible With Secondary Licenses, this\n     License permits You to additionally distribute such Covered Software\n     under the terms of such Secondary License(s), so that the recipient of\n     the Larger Work may, at their option, further distribute the Covered\n     Software under the terms of either this License or such Secondary\n     License(s).\n\n3.4. Notices\n\n     You may not remove or alter the substance of any license notices\n     (including copyright notices, patent notices, disclaimers of warranty, or\n     limitations of liability) contained within the Source Code Form of the\n     Covered Software, except that You may alter any license notices to the\n     extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n     You may choose to offer, and to charge a fee for, warranty, support,\n     indemnity or liability obligations to one or more recipients of Covered\n     Software. However, You may do so only on Your own behalf, and not on\n     behalf of any Contributor. You must make it absolutely clear that any\n     such warranty, support, indemnity, or liability obligation is offered by\n     You alone, and You hereby agree to indemnify every Contributor for any\n     liability incurred by such Contributor as a result of warranty, support,\n     indemnity or liability terms You offer. You may include additional\n     disclaimers of warranty and limitations of liability specific to any\n     jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n   If it is impossible for You to comply with any of the terms of this License\n   with respect to some or all of the Covered Software due to statute,\n   judicial order, or regulation then You must: (a) comply with the terms of\n   this License to the maximum extent possible; and (b) describe the\n   limitations and the code they affect. Such description must be placed in a\n   text file included with all distributions of the Covered Software under\n   this License. Except to the extent prohibited by statute or regulation,\n   such description must be sufficiently detailed for a recipient of ordinary\n   skill to be able to understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n     fail to comply with any of its terms. However, if You become compliant,\n     then the rights granted under this License from a particular Contributor\n     are reinstated (a) provisionally, unless and until such Contributor\n     explicitly and finally terminates Your grants, and (b) on an ongoing\n     basis, if such Contributor fails to notify You of the non-compliance by\n     some reasonable means prior to 60 days after You have come back into\n     compliance. Moreover, Your grants from a particular Contributor are\n     reinstated on an ongoing basis if such Contributor notifies You of the\n     non-compliance by some reasonable means, this is the first time You have\n     received notice of non-compliance with this License from such\n     Contributor, and You become compliant prior to 30 days after Your receipt\n     of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n     infringement claim (excluding declaratory judgment actions,\n     counter-claims, and cross-claims) alleging that a Contributor Version\n     directly or indirectly infringes any patent, then the rights granted to\n     You by any and all Contributors for the Covered Software under Section\n     2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n     license agreements (excluding distributors and resellers) which have been\n     validly granted by You or Your distributors under this License prior to\n     termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n   Covered Software is provided under this License on an \"as is\" basis,\n   without warranty of any kind, either expressed, implied, or statutory,\n   including, without limitation, warranties that the Covered Software is free\n   of defects, merchantable, fit for a particular purpose or non-infringing.\n   The entire risk as to the quality and performance of the Covered Software\n   is with You. Should any Covered Software prove defective in any respect,\n   You (not any Contributor) assume the cost of any necessary servicing,\n   repair, or correction. This disclaimer of warranty constitutes an essential\n   part of this License. No use of  any Covered Software is authorized under\n   this License except under this disclaimer.\n\n7. Limitation of Liability\n\n   Under no circumstances and under no legal theory, whether tort (including\n   negligence), contract, or otherwise, shall any Contributor, or anyone who\n   distributes Covered Software as permitted above, be liable to You for any\n   direct, indirect, special, incidental, or consequential damages of any\n   character including, without limitation, damages for lost profits, loss of\n   goodwill, work stoppage, computer failure or malfunction, or any and all\n   other commercial damages or losses, even if such party shall have been\n   informed of the possibility of such damages. This limitation of liability\n   shall not apply to liability for death or personal injury resulting from\n   such party's negligence to the extent applicable law prohibits such\n   limitation. Some jurisdictions do not allow the exclusion or limitation of\n   incidental or consequential damages, so this exclusion and limitation may\n   not apply to You.\n\n8. Litigation\n\n   Any litigation relating to this License may be brought only in the courts\n   of a jurisdiction where the defendant maintains its principal place of\n   business and such litigation shall be governed by laws of that\n   jurisdiction, without reference to its conflict-of-law provisions. Nothing\n   in this Section shall prevent a party's ability to bring cross-claims or\n   counter-claims.\n\n9. Miscellaneous\n\n   This License represents the complete agreement concerning the subject\n   matter hereof. If any provision of this License is held to be\n   unenforceable, such provision shall be reformed only to the extent\n   necessary to make it enforceable. Any law or regulation which provides that\n   the language of a contract shall be construed against the drafter shall not\n   be used to construe this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n      Mozilla Foundation is the license steward. Except as provided in Section\n      10.3, no one other than the license steward has the right to modify or\n      publish new versions of this License. Each version will be given a\n      distinguishing version number.\n\n10.2. Effect of New Versions\n\n      You may distribute the Covered Software under the terms of the version\n      of the License under which You originally received the Covered Software,\n      or under the terms of any subsequent version published by the license\n      steward.\n\n10.3. Modified Versions\n\n      If you create software not governed by this License, and you want to\n      create a new license for such software, you may create and use a\n      modified version of this License if you rename the license and remove\n      any references to the name of the license steward (except to note that\n      such modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\n      Licenses If You choose to distribute Source Code Form that is\n      Incompatible With Secondary Licenses under the terms of this version of\n      the License, the notice described in Exhibit B of this License must be\n      attached.\n\nExhibit A - Source Code Form License Notice\n\n      This Source Code Form is subject to the\n      terms of the Mozilla Public License, v.\n      2.0. If a copy of the MPL was not\n      distributed with this file, You can\n      obtain one at\n      http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file,\nthen You may include the notice in a location (such as a LICENSE file in a\nrelevant directory) where a recipient would be likely to look for such a\nnotice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n\n      This Source Code Form is \"Incompatible\n      With Secondary Licenses\", as defined by\n      the Mozilla Public License, v. 2.0.\n\n"
  },
  {
    "path": "README.md",
    "content": "# [BLEMeshChat Android](https://github.com/OnlyInAmerica/BLEMeshChat) [![Build Status](https://travis-ci.org/OnlyInAmerica/BLEMeshChat.svg?branch=master)](https://travis-ci.org/OnlyInAmerica/BLEMeshChat)\n\n[![Screenshot](http://i.imgur.com/GMtn5ol.png)](http://i.imgur.com/GMtn5ol.png)\n\n**Under Development : Not yet ready for use!**\n\nAn experiment in pleasant decentralized messaging that works across iOS and Android. This experiment requires Android 5.0 and a device capable of operation as both a Bluetooth LE peripheral and central.\n\nAlso see the [iOS client](https://github.com/chrisballinger/BLEMeshChat) and [Protocol Spec](https://github.com/chrisballinger/BLEMeshChat/wiki).\n\n## Motivation\n\nA system for propagating messages directly from device to device is critical in situations where Internet is unavailable. More abstractly, it may also serve as an interesting model for receiving information influenced by the company you keep.\n\nImagine:\n\nBroadcasting the locations of potable water in a disaster scenario without Internet.\n\nSeeking insulin at a crowded festival where cell service is unreliable.\n\nOrganizing movements of a large protest where cellular Internet is jammed.\n\n## Goals\n\n+ This system must be able to operate constantly in the background without a significant effect on battery life.\n+ Connections to peers must be made without user intervention.\n+ Messages must be signed, and the system must allow the user to verify other users' association with a particular public key.\n\n## License\n\nMPL 2.0"
  },
  {
    "path": "android-wait-for-emulator.sh",
    "content": "#!/bin/bash\n\n# Originally written by Ralf Kistner <ralf@embarkmobile.com>, but placed in the public domain\n\nset +e\n\nbootanim=\"\"\nfailcounter=0\ntimeout_in_sec=360\n\nuntil [[ \"$bootanim\" =~ \"stopped\" ]]; do\n  bootanim=`adb -e shell getprop init.svc.bootanim 2>&1 &`\n  if [[ \"$bootanim\" =~ \"device not found\" || \"$bootanim\" =~ \"device offline\"\n    || \"$bootanim\" =~ \"running\" ]]; then\n    let \"failcounter += 1\"\n    echo \"Waiting for emulator to start\"\n    if [[ $failcounter -gt timeout_in_sec ]]; then\n      echo \"Timeout ($timeout_in_sec seconds) reached; failed to start emulator\"\n      exit 1\n    fi\n  fi\n  sleep 1\ndone\n\necho \"Emulator is ready\"\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'android-apt'\napply plugin: 'com.jakewharton.hugo'\n\nandroid {\n    compileSdkVersion 22\n    buildToolsVersion \"22.0.1\"\n\n    defaultConfig {\n        applicationId \"pro.dbro.ble\"\n        minSdkVersion 21\n        targetSdkVersion 22\n\n        versionCode 1\n        versionName \"1.0\"\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_7\n        targetCompatibility JavaVersion.VERSION_1_7\n    }\n\n    packagingOptions {\n        exclude 'META-INF/LICENSE.txt'\n        exclude 'META-INF/NOTICE.txt'\n    }\n}\n\ndependencies {\n    compile fileTree(dir: 'libs', include: ['*.jar'])\n    compile 'com.android.support:appcompat-v7:22.1.1'\n    compile project(':submodules:airshare:sdk')\n    apt 'net.simonvt.schematic:schematic-compiler:0.5.1'\n    compile 'com.jakewharton.timber:timber:2.7.1'\n    compile 'com.google.guava:guava:18.0'\n    compile 'net.simonvt.schematic:schematic:0.5.3'\n    compile 'com.android.support:support-annotations:20.0.0'\n    compile 'com.jakewharton:butterknife:5.1.2'\n    compile 'com.android.support:recyclerview-v7:21.0.3'\n    compile 'com.android.support:cardview-v7:21.0.0'\n    compile 'com.android.support:palette-v7:21.0.0'\n    compile 'com.nispok:snackbar:2.10.6'\n    compile 'com.facebook.stetho:stetho:1.1.1'\n}\n\napt {\n    arguments {\n        schematicOutPackage 'pro.dbro.ble.schematic'\n    }\n}\n\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "app/src/androidTest/java/pro/dbro/ble/ChatAppTest.java",
    "content": "package pro.dbro.ble;\n\nimport android.app.Application;\nimport android.content.ContentValues;\nimport android.database.Cursor;\nimport android.net.Uri;\nimport android.test.ApplicationTestCase;\n\nimport java.io.IOException;\nimport java.util.Arrays;\nimport java.util.Date;\n\nimport pro.dbro.ble.crypto.KeyPair;\nimport pro.dbro.ble.crypto.SodiumShaker;\nimport pro.dbro.ble.data.ContentProviderStore;\nimport pro.dbro.ble.data.model.ChatContentProvider;\nimport pro.dbro.ble.data.model.DataUtil;\nimport pro.dbro.ble.data.model.Peer;\nimport pro.dbro.ble.data.model.PeerTable;\nimport pro.dbro.ble.protocol.BLEProtocol;\nimport pro.dbro.ble.protocol.IdentityPacket;\nimport pro.dbro.ble.protocol.MessagePacket;\nimport pro.dbro.ble.protocol.OwnedIdentityPacket;\nimport pro.dbro.ble.util.RandomString;\n\n/**\n * Tests of the ChatProtocol and Chat Application.\n */\npublic class ChatAppTest extends ApplicationTestCase<Application> {\n    public ChatAppTest() {\n        super(Application.class);\n    }\n\n    ChatClient mApp;\n    OwnedIdentityPacket mSenderIdentity;\n    boolean mCreatedNewPrimaryIdentity;\n    BLEProtocol bleProtocol = new BLEProtocol();\n    ContentProviderStore dataStore;\n\n    protected void setUp() throws Exception {\n        super.setUp();\n\n        mApp = new ChatClient(getContext());\n        dataStore = new ContentProviderStore(getContext());\n        String username = new RandomString(BLEProtocol.ALIAS_LENGTH).nextString();\n        KeyPair keyPair =  SodiumShaker.generateKeyPair();\n        mSenderIdentity = new OwnedIdentityPacket(keyPair.secretKey, keyPair.publicKey, username, null);\n    }\n\n    @Override\n    protected void tearDown() throws Exception {\n        super.tearDown();\n    }\n\n    /** Protocol Tests **/\n\n    /**\n     * {@link pro.dbro.ble.protocol.IdentityPacket} -> byte[] -> {@link pro.dbro.ble.protocol.IdentityPacket}\n     */\n    public void testCreateAndConsumeIdentityResponse() {\n        byte[] identityResponse = bleProtocol.serializeIdentity(mSenderIdentity);\n\n        // Parse Identity from sender's identityResponse response byte[]\n        IdentityPacket parsedIdentityPacket = bleProtocol.deserializeIdentity(identityResponse);\n\n        assertEquals(parsedIdentityPacket.alias, mSenderIdentity.alias);\n        assertEquals(Arrays.equals(parsedIdentityPacket.publicKey, mSenderIdentity.publicKey), true);\n        assertDateIsRecent(parsedIdentityPacket.dateSeen);\n    }\n\n    /**\n     * {@link pro.dbro.ble.protocol.MessagePacket} -> byte[] -> {@link pro.dbro.ble.protocol.MessagePacket}\n     */\n    public void testCreateAndConsumeMessageResponse() {\n        String messageBody = new RandomString(BLEProtocol.MESSAGE_BODY_LENGTH).nextString();\n\n        MessagePacket messageResponse = bleProtocol.serializeMessage(mSenderIdentity, messageBody);\n\n        MessagePacket parsedMessagePacket = bleProtocol.deserializeMessage(messageResponse.rawPacket);\n\n        assertEquals(messageBody, parsedMessagePacket.body);\n        assertEquals(Arrays.equals(parsedMessagePacket.sender.publicKey, mSenderIdentity.publicKey), true);\n        assertDateIsRecent(parsedMessagePacket.authoredDate);\n    }\n\n    /** Application Tests **/\n\n    /**\n     * Create a {@link pro.dbro.ble.data.model.Peer} for protocol {@link pro.dbro.ble.protocol.IdentityPacket},\n     * then create a {@link pro.dbro.ble.data.model.Message} for protocol {@link pro.dbro.ble.protocol.MessagePacket}.\n     */\n    public void testApplicationIdentityCreationAndMessageConsumption() throws IOException {\n        // TODO : Rewrite for new API\n        // Get or create new primary identity. This Identity serves as the app user\n        Peer user = getOrCreatePrimaryPeerIdentity();\n\n        // User discovers a peer\n\n        IdentityPacket remotePeer = bleProtocol.deserializeIdentity(bleProtocol.serializeIdentity(mSenderIdentity));\n        // Assert Identity response parsed successfully\n        assertEquals(Arrays.equals(remotePeer.publicKey, mSenderIdentity.publicKey), true);\n\n        // Craft a mock message from remote peer\n        String mockReceivedMessageBody = new RandomString(BLEProtocol.MESSAGE_BODY_LENGTH).nextString();\n        MessagePacket mockReceivedMessage = bleProtocol.serializeMessage(mSenderIdentity, mockReceivedMessageBody);\n\n        // User receives mock message from remote peer\n//        pro.dbro.ble.data.model.Message parsedMockReceivedMessage = mApp.consumeReceivedBroadcastMessage(getContext(), mockReceivedMessage);\n//        assertEquals(mockReceivedMessageBody.equals(parsedMockReceivedMessage.getBody()), true);\n\n        // Cleanup\n        // TODO: Should mock database\n        int numDeleted = 0;\n        if (mCreatedNewPrimaryIdentity) {\n            numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Peers.PEERS,\n                    PeerTable.id + \" = ?\",\n                    new String[]{String.valueOf(user.getId())});\n            assertEquals(numDeleted, 1);\n            numDeleted = 0;\n        }\n\n//        numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Peers.PEERS,\n//                PeerTable.id + \" = ?\",\n//                new String[] {String.valueOf(remotePeer.getId())});\n//\n//        assertEquals(numDeleted, 1);\n//        numDeleted = 0;\n//\n//        numDeleted = getContext().getContentResolver().delete(ChatContentProvider.Messages.MESSAGES,\n//                MessageTable.id + \" = ?\",\n//                new String[] {String.valueOf(parsedMockReceivedMessage.getId())});\n//        assertEquals(numDeleted, 1);\n//        numDeleted = 0;\n    }\n\n    /**\n     * Test database lookups by BLOB column\n     */\n    public void testDatabaseQueryByBlob() {\n        byte[] fakePubKey = new byte[] { (byte) 0x01 };\n        ContentValues stubPeer = new ContentValues();\n        stubPeer.put(PeerTable.alias, \"test\");\n        stubPeer.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date()));\n        stubPeer.put(PeerTable.pubKey, fakePubKey);\n        Uri stubPeerUri = getContext().getContentResolver().insert(ChatContentProvider.Peers.PEERS, stubPeer);\n\n        int stubPeerId = Integer.parseInt(stubPeerUri.getLastPathSegment());\n\n        Cursor result = getContext().getContentResolver().query(ChatContentProvider.Peers.PEERS,\n                null,\n                PeerTable.id + \" = ?\",\n                new String[] {\n                        String.valueOf(stubPeerId)\n                },\n                null);\n\n        assertEquals(result != null, true);\n        assertEquals(result.moveToFirst(), true);\n\n        byte[] resultBlob = result.getBlob(result.getColumnIndex(PeerTable.pubKey));\n\n        assertEquals(Arrays.equals(resultBlob, fakePubKey), true);\n        result.close();\n\n        result = getContext().getContentResolver().query(ChatContentProvider.Peers.PEERS,\n                null,\n                \"quote(\" + PeerTable.pubKey + \") = ?\",\n                new String[] {\n                  \"X'01'\"\n                },\n                null);\n\n        assertEquals(result != null, true);\n        assertEquals(result.moveToFirst(), true);\n\n        // Cleanup\n\n        int numDeleted = getContext().getContentResolver().delete((ChatContentProvider.Peers.PEERS),\n                PeerTable.id + \" = ?\",\n                new String[] {\n                        String.valueOf(stubPeerId)\n                });\n        assertEquals(numDeleted ,1);\n    }\n    /** Utility **/\n\n    private Peer getOrCreatePrimaryPeerIdentity() throws IOException {\n        Peer user = mApp.getPrimaryLocalPeer();\n        if (user == null) {\n            mCreatedNewPrimaryIdentity = true;\n            user =  mApp.createPrimaryIdentity(new RandomString(BLEProtocol.ALIAS_LENGTH).nextString());\n        }\n        return user;\n    }\n\n    private void assertDateIsRecent(Date mustBeRecent) {\n        long now = new Date().getTime();\n        long oneSecondAgo = now - 1000;\n\n        if ( (mustBeRecent.getTime() > now) ){\n            throw new IllegalStateException(\"Parsed Identity time is from the future \" + mustBeRecent);\n\n        } else if (mustBeRecent.getTime() < oneSecondAgo) {\n            throw new IllegalStateException(\"Parsed Identity time is from more than 500ms ago \" + mustBeRecent);\n        }\n    }\n}"
  },
  {
    "path": "app/src/androidTest/java/pro/dbro/ble/util/RandomString.java",
    "content": "package pro.dbro.ble.util;\n\nimport java.util.Random;\n\npublic class RandomString {\n\n    private static final char[] symbols;\n\n    static {\n        StringBuilder tmp = new StringBuilder();\n        for (char ch = '0'; ch <= '9'; ++ch)\n            tmp.append(ch);\n        for (char ch = 'a'; ch <= 'z'; ++ch)\n            tmp.append(ch);\n        symbols = tmp.toString().toCharArray();\n    }\n\n    private final Random random = new Random();\n\n    private final char[] buf;\n\n    public RandomString(int length) {\n        if (length < 1)\n            throw new IllegalArgumentException(\"length < 1: \" + length);\n        buf = new char[length];\n    }\n\n    public String nextString() {\n        for (int idx = 0; idx < buf.length; ++idx)\n            buf[idx] = symbols[random.nextInt(symbols.length)];\n        return new String(buf);\n    }\n}"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"pro.dbro.ble\" >\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:theme=\"@style/AppTheme\"\n        android:name=\".ChatApp\">\n\n        <provider\n            android:name=\".schematic.ChatContentProvider\"\n            android:authorities=\"pro.dbro.ble.chatprovider\"\n            android:exported=\"true\">\n        </provider>\n\n        <service android:name=\"pro.dbro.airshare.app.AirShareService\" />\n\n        <activity\n            android:name=\".ui.activities.MainActivity\"\n            android:screenOrientation=\"portrait\"\n            android:windowSoftInputMode=\"adjustResize\">\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    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/java/com/google/samples/apps/iosched/ui/widget/ScrimInsetsScrollView.java",
    "content": "/*\n * Copyright 2014 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 */\n\npackage com.google.samples.apps.iosched.ui.widget;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.graphics.Canvas;\nimport android.graphics.Rect;\nimport android.graphics.drawable.Drawable;\nimport android.support.v4.view.ViewCompat;\nimport android.util.AttributeSet;\nimport android.widget.ScrollView;\n\nimport pro.dbro.ble.R;\n\n/**\n * A layout that draws something in the insets passed to {@link #fitSystemWindows(Rect)}, i.e. the area above UI chrome\n * (status and navigation bars, overlay action bars).\n */\npublic class ScrimInsetsScrollView extends ScrollView {\n    private Drawable mInsetForeground;\n\n    private Rect mInsets;\n    private Rect mTempRect = new Rect();\n    private OnInsetsCallback mOnInsetsCallback;\n\n    public ScrimInsetsScrollView(Context context) {\n        super(context);\n        init(context, null, 0);\n    }\n\n    public ScrimInsetsScrollView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init(context, attrs, 0);\n    }\n\n    public ScrimInsetsScrollView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        init(context, attrs, defStyle);\n    }\n\n    private void init(Context context, AttributeSet attrs, int defStyle) {\n        final TypedArray a = context.obtainStyledAttributes(attrs,\n                R.styleable.ScrimInsetsView, defStyle, 0);\n        if (a == null) {\n            return;\n        }\n        mInsetForeground = a.getDrawable(R.styleable.ScrimInsetsView_insetForeground);\n        a.recycle();\n\n        setWillNotDraw(true);\n    }\n\n    @Override\n    protected boolean fitSystemWindows(Rect insets) {\n        mInsets = new Rect(insets);\n        setWillNotDraw(mInsetForeground == null);\n        ViewCompat.postInvalidateOnAnimation(this);\n        if (mOnInsetsCallback != null) {\n            mOnInsetsCallback.onInsetsChanged(insets);\n        }\n        return true; // consume insets\n    }\n\n    @Override\n    public void draw(Canvas canvas) {\n        super.draw(canvas);\n\n        int width = getWidth();\n        int height = getHeight();\n        if (mInsets != null && mInsetForeground != null) {\n            int sc = canvas.save();\n            canvas.translate(getScrollX(), getScrollY());\n\n            // Top\n            mTempRect.set(0, 0, width, mInsets.top);\n            mInsetForeground.setBounds(mTempRect);\n            mInsetForeground.draw(canvas);\n\n            // Bottom\n            mTempRect.set(0, height - mInsets.bottom, width, height);\n            mInsetForeground.setBounds(mTempRect);\n            mInsetForeground.draw(canvas);\n\n            // Left\n            mTempRect.set(0, mInsets.top, mInsets.left, height - mInsets.bottom);\n            mInsetForeground.setBounds(mTempRect);\n            mInsetForeground.draw(canvas);\n\n            // Right\n            mTempRect.set(width - mInsets.right, mInsets.top, width, height - mInsets.bottom);\n            mInsetForeground.setBounds(mTempRect);\n            mInsetForeground.draw(canvas);\n\n            canvas.restoreToCount(sc);\n        }\n    }\n\n    @Override\n    protected void onAttachedToWindow() {\n        super.onAttachedToWindow();\n        if (mInsetForeground != null) {\n            mInsetForeground.setCallback(this);\n        }\n    }\n\n    @Override\n    protected void onDetachedFromWindow() {\n        super.onDetachedFromWindow();\n        if (mInsetForeground != null) {\n            mInsetForeground.setCallback(null);\n        }\n    }\n\n    /**\n     * Allows the calling container to specify a callback for custom processing when insets change (i.e. when\n     * {@link #fitSystemWindows(Rect)} is called. This is useful for setting padding on UI elements based on\n     * UI chrome insets (e.g. a Google Map or a ListView). When using with ListView or GridView, remember to set\n     * clipToPadding to false.\n     */\n    public void setOnInsetsCallback(OnInsetsCallback onInsetsCallback) {\n        mOnInsetsCallback = onInsetsCallback;\n    }\n\n    public static interface OnInsetsCallback {\n        public void onInsetsChanged(Rect insets);\n    }\n}"
  },
  {
    "path": "app/src/main/java/im/delight/android/identicons/AsymmetricIdenticon.java",
    "content": "package im.delight.android.identicons;\n\n/**\n * Copyright 2014 www.delight.im <info@delight.im>\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 */\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.util.AttributeSet;\n\npublic class AsymmetricIdenticon extends Identicon {\n\n\tpublic AsymmetricIdenticon(Context context) {\n\t\tsuper(context);\n\t}\n\n\tpublic AsymmetricIdenticon(Context context, AttributeSet attrs) {\n\t\tsuper(context, attrs);\n\t}\n\n\tpublic AsymmetricIdenticon(Context context, AttributeSet attrs, int defStyleAttr) {\n\t\tsuper(context, attrs, defStyleAttr);\n\t}\n\n\t@Override\n\tprotected boolean isCellVisible(int row, int column) {\n\t\treturn getByte(3 + row * getColumnCount() + column) >= 0;\n\t}\n\n\t@Override\n\tprotected int getIconColor() {\n\t\treturn Color.rgb(getByte(0)+128, getByte(1)+128, getByte(2)+128);\n\t}\n\n\t@Override\n\tprotected int getRowCount() {\n\t\treturn 4;\n\t}\n\n\t@Override\n\tprotected int getColumnCount() {\n\t\treturn 4;\n\t}\n\n}\n"
  },
  {
    "path": "app/src/main/java/im/delight/android/identicons/Identicon.java",
    "content": "package im.delight.android.identicons;\n\n/**\n * Copyright 2014 www.delight.im <info@delight.im>\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 */\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.view.View;\n\nimport java.security.MessageDigest;\n\nabstract public class Identicon extends View {\n\t\n\tprivate static final String HASH_ALGORITHM = \"SHA-256\";\n\tprivate final int mRowCount;\n\tprivate final int mColumnCount;\n\tprivate final Paint mPaint;\n\tprivate volatile int mCellWidth;\n\tprivate volatile int mCellHeight;\n\tprivate volatile byte[] mHash;\n\tprivate volatile int[][] mColors;\n\tprivate volatile boolean mReady;\n\n\tpublic Identicon(Context context) {\n\t\tsuper(context);\n\n\t\tmRowCount = getRowCount();\n\t\tmColumnCount = getColumnCount();\n\t\tmPaint = new Paint();\n\n\t\tinit();\n\t}\n\n\tpublic Identicon(Context context, AttributeSet attrs) {\n\t\tsuper(context, attrs);\n\n\t\tmRowCount = getRowCount();\n\t\tmColumnCount = getColumnCount();\n\t\tmPaint = new Paint();\n\n\t\tinit();\n\t}\n\n\tpublic Identicon(Context context, AttributeSet attrs, int defStyleAttr) {\n\t\tsuper(context, attrs, defStyleAttr);\n\n\t\tmRowCount = getRowCount();\n\t\tmColumnCount = getColumnCount();\n\t\tmPaint = new Paint();\n\n\t\tinit();\n\t}\n\t\t\n\t@SuppressLint(\"NewApi\")\n\tprotected void init() {\n\t\tmPaint.setStyle(Paint.Style.FILL);\n\t\tmPaint.setAntiAlias(true);\n\t\tmPaint.setDither(true);\n\t\t\n\t\tsetWillNotDraw(false);\n\t\tif (Build.VERSION.SDK_INT >= 11) {\n\t\t\tsetLayerType(View.LAYER_TYPE_SOFTWARE, null);\n\t\t}\n\t}\n\n\tpublic void show(String input) {\n\t\t// if the input was null\n\t\tif (input == null) {\n\t\t\t// we can't create a hash value and have nothing to show (draw to the view)\n\t\t\tmHash = null;\n\t\t}\n\t\t// if the input was a proper string (non-null)\n\t\telse {\n\t\t\t// generate a hash from the string to get unique but deterministic byte values \n\t\t\ttry {\n\t\t\t\tfinal MessageDigest digest = MessageDigest.getInstance(HASH_ALGORITHM);\n\t\t\t\tdigest.update(input == null ? new byte[0] : input.getBytes());\n\t\t\t\tmHash = digest.digest();\n\t\t\t}\n\t\t\tcatch (Exception e) {\n\t\t\t\tmHash = null;\n\t\t\t}\n\t\t}\n\t\t\n\t\t// set up the cell colors according to the input that was provided via show(...)\n\t\tsetupColors();\n\t\t\n\t\t// this view may now be drawn (and thus must be re-drawn)\n\t\tmReady = true;\n\t\tinvalidate();\n\t}\n\t\n\tpublic void show(int input) {\n\t\tshow(String.valueOf(input));\n\t}\n\t\n\tpublic void show(long input) {\n\t\tshow(String.valueOf(input));\n\t}\n\t\n\tpublic void show(float input) {\n\t\tshow(String.valueOf(input));\n\t}\n\t\n\tpublic void show(double input) {\n\t\tshow(String.valueOf(input));\n\t}\n\t\n\tpublic void show(byte input) {\n\t\tshow(String.valueOf(input));\n\t}\n\t\n\tpublic void show(char input) {\n\t\tshow(String.valueOf(input));\n\t}\n\t\n\tpublic void show(boolean input) {\n\t\tshow(String.valueOf(input));\n\t}\n\t\n\tpublic void show(Object input) {\n\t\tif (input == null) {\n\t\t\tmHash = null;\n\t\t}\n\t\telse {\n\t\t\tshow(String.valueOf(input));\n\t\t}\n\t}\n\t\n\tprotected void setupColors() {\n\t\tmColors = new int[mRowCount][mColumnCount];\n\t\tint colorVisible = getIconColor();\n\n\t\tfor (int r = 0; r < mRowCount; r++) {\n\t\t\tfor (int c = 0; c < mColumnCount; c++) {\n\t\t\t\tif (isCellVisible(r, c)) {\n\t\t\t\t\tmColors[r][c] = colorVisible;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tmColors[r][c] = Color.TRANSPARENT;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t\n\tprotected byte getByte(int index) {\n\t\tif (mHash == null) {\n\t\t\treturn -128;\n\t\t}\n\t\telse {\n\t\t\treturn mHash[index % mHash.length];\n\t\t}\n\t}\n\t\n\tabstract protected int getRowCount();\n\t\n\tabstract protected int getColumnCount();\n\n\tabstract protected boolean isCellVisible(int row, int column);\n\n\tabstract protected int getIconColor();\n\t\n\t@Override\n\tprotected void onSizeChanged(int w, int h, int oldw, int oldh) {\n\t\tsuper.onSizeChanged(w, h, oldw, oldh);\n\t\t\n\t\tmCellWidth = w / mColumnCount;\n\t\tmCellHeight = h / mRowCount;\n\t}\n\t\n\t@Override\n\tprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n\t\tsuper.onMeasure(widthMeasureSpec, heightMeasureSpec);\n\t\tint size = Math.min(getMeasuredWidth(), getMeasuredHeight());\n\t\tsetMeasuredDimension(size, size);\n\t}\n\t\n\t@Override\n\tprotected void onDraw(Canvas canvas) {\n\t\tsuper.onDraw(canvas);\n\t\tif (mReady) {\n\t\t\tint x, y;\n\t\t\tfor (int r = 0; r < mRowCount; r++) {\n\t\t\t\tfor (int c = 0; c < mColumnCount; c++) {\n\t\t\t\t\tx = mCellWidth * c;\n\t\t\t\t\ty = mCellHeight * r;\n\t\t\t\t\t\n\t\t\t\t\tmPaint.setColor(mColors[r][c]);\n\n\t\t\t\t\tcanvas.drawRect(x, y + mCellHeight, x + mCellWidth, y, mPaint);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "app/src/main/java/im/delight/android/identicons/SymmetricIdenticon.java",
    "content": "package im.delight.android.identicons;\n\n/**\n * Copyright 2014 www.delight.im <info@delight.im>\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 */\n\nimport android.content.Context;\nimport android.graphics.Color;\nimport android.util.AttributeSet;\n\npublic class SymmetricIdenticon extends Identicon {\n\t\n\tprivate static final int CENTER_COLUMN_INDEX = 3;\n\n\tpublic SymmetricIdenticon(Context context) {\n\t\tsuper(context);\n\t}\n\n\tpublic SymmetricIdenticon(Context context, AttributeSet attrs) {\n\t\tsuper(context, attrs);\n\t}\n\n\tpublic SymmetricIdenticon(Context context, AttributeSet attrs, int defStyleAttr) {\n\t\tsuper(context, attrs, defStyleAttr);\n\t}\n\t\n\tprotected int getSymmetricColumnIndex(int row) {\n\t\tif (row < CENTER_COLUMN_INDEX) {\n\t\t\treturn row;\n\t\t}\n\t\telse {\n\t\t\treturn getColumnCount() - row - 1;\n\t\t}\n\t}\n\n\t@Override\n\tprotected boolean isCellVisible(int row, int column) {\n\t\treturn getByte(3 + row * CENTER_COLUMN_INDEX + getSymmetricColumnIndex(column)) >= 0;\n\t}\n\n\t@Override\n\tprotected int getIconColor() {\n\t\treturn Color.rgb(getByte(0)+128, getByte(1)+128, getByte(2)+128);\n\t}\n\n\t@Override\n\tprotected int getRowCount() {\n\t\treturn 5;\n\t}\n\n\t@Override\n\tprotected int getColumnCount() {\n\t\treturn 5;\n\t}\n\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ActivityRecevingMessagesIndicator.java",
    "content": "package pro.dbro.ble;\n\n/**\n * Implemented by a Service or other entity to report an Activity is bound, and thus\n * in the foreground. e.g: Useful to determine whether to post message notifications.\n *\n * Created by davidbrodsky on 11/14/14.\n */\npublic interface ActivityRecevingMessagesIndicator {\n\n    public boolean isActivityReceivingMessages();\n\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ChatApp.java",
    "content": "package pro.dbro.ble;\n\nimport android.app.Application;\n\nimport com.facebook.stetho.Stetho;\n\nimport timber.log.Timber;\n\n/**\n * Created by davidbrodsky on 4/17/15.\n */\npublic class ChatApp extends Application {\n\n    @Override public void onCreate() {\n        super.onCreate();\n\n        if (BuildConfig.DEBUG) {\n            Timber.plant(new Timber.DebugTree());\n\n            Stetho.initialize(\n                    Stetho.newInitializerBuilder(this)\n                            .enableDumpapp(\n                                    Stetho.defaultDumperPluginsProvider(this))\n                            .enableWebKitInspector(\n                                    Stetho.defaultInspectorModulesProvider(this))\n                            .build());\n        }\n\n        // If we abandon Timber logging in this app, enable below line\n        // to enable Timber logging in sdk\n        //Logging.forceLogging();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ChatClient.java",
    "content": "package pro.dbro.ble;\n\nimport android.content.Context;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\nimport com.google.common.collect.BiMap;\nimport com.google.common.collect.HashBiMap;\n\nimport java.util.HashMap;\n\nimport pro.dbro.airshare.app.AirShareService;\nimport pro.dbro.airshare.transport.Transport;\nimport pro.dbro.ble.data.ContentProviderStore;\nimport pro.dbro.ble.data.DataStore;\nimport pro.dbro.ble.data.model.DataUtil;\nimport pro.dbro.ble.data.model.Message;\nimport pro.dbro.ble.data.model.Peer;\nimport pro.dbro.ble.protocol.BLEProtocol;\nimport pro.dbro.ble.protocol.MessagePacket;\nimport pro.dbro.ble.protocol.OwnedIdentityPacket;\nimport pro.dbro.ble.protocol.Protocol;\nimport pro.dbro.ble.ui.Notification;\nimport pro.dbro.ble.ui.activities.LogConsumer;\nimport timber.log.Timber;\n\n/**\n * Created by davidbrodsky on 10/13/14.\n */\npublic class ChatClient implements AirShareService.Callback,\n                                   ChatPeerFlow.DataOutlet,\n                                   ChatPeerFlow.Callback {\n\n    public interface Callback {\n        /** Client should not invoke remotePeer#close() */\n        void onAppPeerStatusUpdated(@NonNull Peer remotePeer,\n                                    @NonNull ConnectionStatus status);\n    }\n\n    public static final String TAG = \"ChatApp\";\n    public static final String AIRSHARE_SERVICE_NAME = \"BLEMeshChat\";\n\n    private Context   mContext;\n    private DataStore mDataStore;\n    private Protocol  mProtocol;\n    private AirShareService.ServiceBinder mAirShareServiceBinder;\n    private Callback mCallback;\n\n    private HashMap<pro.dbro.airshare.session.Peer, ChatPeerFlow> mFlows = new HashMap<>();\n\n    /** AirShare Peer -> BLEMeshChat Peer id */\n    private BiMap<pro.dbro.airshare.session.Peer, Integer> mConnectedPeers = HashBiMap.create();\n\n    // <editor-fold desc=\"Public API\">\n\n    public ChatClient(@NonNull Context context) {\n        mContext = context;\n\n        mProtocol  = new BLEProtocol();\n        mDataStore = new ContentProviderStore(context);\n    }\n\n    public void setAirShareServiceBinder(AirShareService.ServiceBinder binder) {\n        mAirShareServiceBinder = binder;\n        mAirShareServiceBinder.setCallback(this);\n    }\n\n    public void setCallback(Callback callback) {\n        mCallback = callback;\n    }\n\n    // <editor-fold desc=\"Identity & Availability\">\n\n    public void makeAvailable() {\n        if (mDataStore.getPrimaryLocalPeer() == null) {\n            Timber.e(\"No primary Identity. Cannot make client available\");\n            return;\n        }\n\n        if (mAirShareServiceBinder == null) {\n            Timber.e(\"No AirShareBinder set. Cannot make available\");\n            return;\n        }\n\n        mAirShareServiceBinder.advertiseLocalUser();\n        mAirShareServiceBinder.scanForOtherUsers();\n    }\n\n    public void makeUnavailable() {\n        if (mAirShareServiceBinder == null) {\n            Timber.e(\"No AirShareBinder set. Cannot make unavailable\");\n            return;\n        }\n\n        mAirShareServiceBinder.stop();\n    }\n\n    public Peer getPrimaryLocalPeer() {\n        return mDataStore.getPrimaryLocalPeer();\n    }\n\n    public Peer createPrimaryIdentity(String alias) {\n        // TODO Test if this should be moved to background thread and async call?\n        return mDataStore.createLocalPeerWithAlias(alias, mProtocol);\n    }\n\n    // </editor-fold desc=\"Identity & Availability\">\n\n    // <editor-fold desc=\"Messages\">\n\n    public void sendPublicMessageFromPrimaryIdentity(String body) {\n        MessagePacket messagePacket = mProtocol.serializeMessage((OwnedIdentityPacket) getPrimaryLocalPeer().getIdentity(), body);\n        mDataStore.createOrUpdateMessageWithProtocolMessage(messagePacket).close();\n        // TODO : Send to connected peers. Future peers will get message during flow\n        if (mAirShareServiceBinder != null) {\n\n            for (pro.dbro.airshare.session.Peer peer : mConnectedPeers.keySet()) {\n                ChatPeerFlow flow = mFlows.get(peer);\n                // If we're actively flowing with a peer, add the message to that flow\n                // else, send immediately\n                if (flow != null && !flow.isComplete())\n                    flow.queueMessage(messagePacket);\n                else\n                    mAirShareServiceBinder.send(messagePacket.rawPacket, peer);\n            }\n\n        }\n    }\n\n    // </editor-fold desc=\"Messages\">\n\n    public DataStore getDataStore() {\n        return mDataStore;\n    }\n\n    // </editor-fold desc=\"Public API\">\n\n    // <editor-fold desc=\"Private API\">\n\n    @Override\n    public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow,\n                                       @NonNull Peer remotePeer,\n                                       @NonNull ConnectionStatus status) {\n\n        Timber.d(\"%s %s\", remotePeer.getAlias(), status == ConnectionStatus.CONNECTED ? \"connected\" : \"disconnected\");\n        if (mCallback != null)\n            mCallback.onAppPeerStatusUpdated(remotePeer, status);\n\n        if (!mAirShareServiceBinder.isActivityReceivingMessages())\n            Notification.displayPeerAvailableNotification(mContext, remotePeer, status == ConnectionStatus.CONNECTED);\n\n        switch (status) {\n            case CONNECTED:\n                mConnectedPeers.put(flow.getRemoteAirSharePeer(), remotePeer.getId());\n                break;\n\n            case DISCONNECTED:\n                mConnectedPeers.remove(flow.getRemoteAirSharePeer());\n                break;\n        }\n    }\n\n    @Override\n    public void onMessageSent(@NonNull ChatPeerFlow flow, @NonNull Message message, @NonNull Peer recipient) {\n        Timber.d(\"Sent message: '%s'\", message.getBody());\n        // TODO : Might be unnecessary\n        message.close();\n    }\n\n    @Override\n    public void onMessageReceived(@NonNull ChatPeerFlow flow, @NonNull Message message, Peer sender) {\n        Timber.d(\"Received message: '%s' with sig '%s' \", message.getBody(), DataUtil.bytesToHex(message.getSignature()).substring(0, 3));\n\n        // We don't check that mAirShareServiceBinder is not null because this callback is provoked\n        // by the binder callbacks\n\n        // Send message notification if it's a new message and no Activity is reported active\n        if (!mAirShareServiceBinder.isActivityReceivingMessages()) {\n            Notification.displayMessageNotification(mContext, message, sender);\n            message.close();\n        }\n    }\n\n    @Override\n    public void onDataRecevied(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer sender, @Nullable Exception exception) {\n        ChatPeerFlow flow = mFlows.get(sender);\n\n        if (flow == null) {\n            Timber.w(\"No flow for %s\", sender.getAlias());\n            return;\n        }\n\n        try {\n            flow.onDataReceived(data);\n        } catch (ChatPeerFlow.UnexpectedDataException e) {\n            Timber.e(e, \"Error processing received data\");\n        }\n    }\n\n    @Override\n    public void onDataSent(@NonNull AirShareService.ServiceBinder binder, @Nullable byte[] data, @NonNull pro.dbro.airshare.session.Peer recipient, @Nullable Exception exception) {\n        ChatPeerFlow flow = mFlows.get(recipient);\n\n        if (flow == null) {\n            Timber.w(\"No flow for %s\", recipient.getAlias());\n            return;\n        }\n\n        try {\n            flow.onDataSent(data);\n        } catch (ChatPeerFlow.UnexpectedDataException e) {\n            Timber.e(e, \"Error processing sent data\");\n        }\n    }\n\n    @Override\n    public void onPeerStatusUpdated(@NonNull AirShareService.ServiceBinder binder, @NonNull pro.dbro.airshare.session.Peer peer, @NonNull Transport.ConnectionStatus newStatus, boolean peerIsHost) {\n        if (newStatus == Transport.ConnectionStatus.CONNECTED) {\n            mConnectedPeers.put(peer, null); // We will add the BLEMeshChat peer id after identity is received\n            Timber.d(\"Beginning flow with %s as %s\", peer.getAlias(), peerIsHost ? \"host\" : \"client\");\n            mFlows.put(peer, new ChatPeerFlow(mDataStore, mProtocol, this, peer, peerIsHost, this));\n        }\n        else if (newStatus == Transport.ConnectionStatus.DISCONNECTED) {\n\n            if (!mConnectedPeers.containsKey(peer) || mConnectedPeers.get(peer) == null) {\n                if (mConnectedPeers.containsKey(peer)) mConnectedPeers.remove(peer);\n                Timber.w(\"Cannot report peer %s disconnected, no connection record\", peer.getAlias());\n                return;\n            }\n\n            int blePeerId = mConnectedPeers.get(peer);\n            Peer remotePeer = mDataStore.getPeerById(blePeerId);\n            onAppPeerStatusUpdated(mFlows.get(peer), remotePeer, ConnectionStatus.DISCONNECTED);\n        }\n    }\n\n    @Override\n    public void onPeerTransportUpdated(@NonNull AirShareService.ServiceBinder binder,\n                                       @NonNull pro.dbro.airshare.session.Peer peer,\n                                       int newTransportCode,\n                                       @Nullable Exception exception) {\n        // unused. The networking demands of this app appear to works fine over BLE\n    }\n\n    @Override\n    public void sendData(pro.dbro.airshare.session.Peer peer, byte[] data) {\n        if(mAirShareServiceBinder == null) {\n            Timber.e(\"AirShare Service binder is null! Cannot send data\");\n            return;\n        }\n        mAirShareServiceBinder.send(data, peer);\n    }\n\n    // </editor-fold desc=\"Private API\">\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ChatPeerFlow.java",
    "content": "package pro.dbro.ble;\n\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\nimport java.util.ArrayDeque;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport pro.dbro.airshare.session.Peer;\nimport pro.dbro.ble.data.DataStore;\nimport pro.dbro.ble.data.model.DataUtil;\nimport pro.dbro.ble.data.model.Message;\nimport pro.dbro.ble.data.model.MessageCollection;\nimport pro.dbro.ble.protocol.IdentityPacket;\nimport pro.dbro.ble.protocol.MessagePacket;\nimport pro.dbro.ble.protocol.NoDataPacket;\nimport pro.dbro.ble.protocol.OwnedIdentityPacket;\nimport pro.dbro.ble.protocol.Protocol;\nimport timber.log.Timber;\n\n/**\n * This class orchestrates the flow between two ChatApp Peers, handing network requests and\n * updating the {@link pro.dbro.ble.data.DataStore}. The client of this class may use\n * {@link ChatPeerFlow.Callback} to update their UI or in-memory application state.\n *\n * The general gist of the flow:\n *\n * 1) Client peer writes identity\n * 2) Client peer waits for host identity\n * 3) Client peer writes outgoing messages\n * 4) Client peer waits for incoming messages\n * Created by davidbrodsky on 4/16/15.\n */\npublic class ChatPeerFlow {\n\n    public static class UnexpectedDataException extends Exception {\n        public UnexpectedDataException(String detailMessage) {\n            super(detailMessage);\n        }\n    }\n\n    /** Entity responsible for sending data to a peer */\n    public static interface DataOutlet {\n        public void sendData(Peer peer, byte[] data);\n    }\n\n    public static interface Callback {\n\n        public static enum ConnectionStatus { CONNECTED, DISCONNECTED }\n\n        public void onAppPeerStatusUpdated(@NonNull ChatPeerFlow flow,\n                                           @NonNull pro.dbro.ble.data.model.Peer peer,\n                                           @NonNull ConnectionStatus status);\n\n        public void onMessageSent(@NonNull ChatPeerFlow flow,\n                                  @NonNull Message message,\n                                  @NonNull pro.dbro.ble.data.model.Peer recipient);\n\n        public void onMessageReceived(@NonNull ChatPeerFlow flow,\n                                      @NonNull Message message,\n                                      @Nullable pro.dbro.ble.data.model.Peer sender);\n\n    }\n\n    private static final int MESSAGES_PER_RESPONSE = 50;\n    private static final int IDENTITIES_PER_RESPONSE = 10;\n    public static enum State { CLIENT_WRITE_ID, HOST_WRITE_ID, CLIENT_WRITE_MSGS, HOST_WRITE_MSGS }\n\n    private State mState = State.CLIENT_WRITE_ID;\n    private OwnedIdentityPacket mLocalIdentity;\n    private Peer mRemoteAirSharePeer;\n    private Protocol mProtocol;\n    private DataStore mDataStore;\n    private DataOutlet mOutlet;\n    private IdentityPacket mRemoteIdentity;\n    private Callback mCallback;\n    private ArrayDeque<MessagePacket> mMessageOutbox = new ArrayDeque<>();\n    private ArrayDeque<IdentityPacket> mIdentityOutbox = new ArrayDeque<>();\n\n    private boolean mPeerIsHost;\n    private boolean mIsComplete = false;\n    private boolean mFetchedMessages = false;\n    private boolean mFetchedIdentities = false;\n    private boolean mGotRemotePeerIdentity = false;\n\n    public ChatPeerFlow(DataStore dataStore,\n                        Protocol protocol,\n                        DataOutlet outlet,\n                        Peer remotePeer,\n                        boolean peerIsHost,\n                        Callback callback) {\n\n        mRemoteAirSharePeer = remotePeer;\n        mOutlet = outlet;\n        mProtocol = protocol;\n        mDataStore = dataStore;\n        mLocalIdentity = (OwnedIdentityPacket) dataStore.getPrimaryLocalPeer().getIdentity();\n        mPeerIsHost = peerIsHost;\n        mCallback = callback;\n\n        // Client initiates flow\n        if (mPeerIsHost)\n            sendIdentity();\n    }\n\n    public boolean isComplete() {\n        return mIsComplete;\n    }\n\n    public Peer getRemoteAirSharePeer() {\n        return mRemoteAirSharePeer;\n    }\n\n    public void queueMessage(MessagePacket message) {\n        mMessageOutbox.add(message);\n    }\n\n    /**\n     * Called when data is acknowledged as sent to the peer passed to this instance's constructor\n     * @return whether this flow is complete and should not receive further events.\n     */\n    public boolean onDataSent(byte[] data) throws UnexpectedDataException {\n        // When data is ack'd we should be in a local-peer writing state\n        if ((!mPeerIsHost && (mState == State.CLIENT_WRITE_ID || (mState == State.CLIENT_WRITE_MSGS && !mIsComplete))) ||\n            (mPeerIsHost && (mState == State.HOST_WRITE_ID || (mState == State.HOST_WRITE_MSGS && !mIsComplete)))) {\n\n            throw new IllegalStateException(String.format(\"onDataSent invalid state %s for local as %s\", mState, mPeerIsHost ? \"client\" : \"host\"));\n\n        }\n\n        Timber.d(\"Sent data %s\", DataUtil.bytesToHex(data));\n\n        byte type = mProtocol.getPacketType(data);\n\n        // TODO : Perhaps we should cache last sent item to avoid deserializing bytes we've\n        // just serialized in sendData\n        switch (mState) {\n            case HOST_WRITE_ID:\n            case CLIENT_WRITE_ID:\n\n                switch(type) {\n                    case IdentityPacket.TYPE:\n\n                        IdentityPacket sentIdPkt = mProtocol.deserializeIdentity(data);\n                        mDataStore.createOrUpdateRemotePeerWithProtocolIdentity(sentIdPkt);\n                        // We can only report the identity sent once we know the peer's identity\n                        // We also always want to send our own identity first\n                        if (mRemoteIdentity != null) {\n                            Timber.d(\"Marked identity %s delivered to %s\", sentIdPkt.alias, mRemoteIdentity.alias);\n                            mDataStore.markIdentityDeliveredToPeer(sentIdPkt, mRemoteIdentity);\n                        }\n\n                        mIdentityOutbox.poll();\n\n                        sendAsAppropriate();\n                        break;\n\n                    case NoDataPacket.TYPE:\n\n                        incrementStateAndSendAsAppropriate();\n                        break;\n\n                    default:\n                        throw new UnexpectedDataException(String.format(\"Expected IdentityPacket (type %d). Got type %d\", IdentityPacket.TYPE, type));\n\n                }\n                break;\n\n            case HOST_WRITE_MSGS:\n            case CLIENT_WRITE_MSGS:\n\n                switch(type) {\n                    case MessagePacket.TYPE:\n\n                        MessagePacket msgPkt = mProtocol.deserializeMessageWithIdentity(data, mRemoteIdentity);\n                        Message msg = mDataStore.createOrUpdateMessageWithProtocolMessage(msgPkt);\n                        // Mark incoming messages as delivered to sender\n                        mDataStore.markMessageDeliveredToPeer(msgPkt, mRemoteIdentity);\n                        mCallback.onMessageSent(this, msg, mDataStore.getPeerByPubKey(mRemoteIdentity.publicKey));\n\n                        mMessageOutbox.poll();\n\n                        sendAsAppropriate();\n                        break;\n\n                    case NoDataPacket.TYPE:\n\n                        incrementStateAndSendAsAppropriate();\n                        break;\n\n                    default:\n                        throw new UnexpectedDataException(String.format(\"Expected MessagePacket (type %d). Got type %d\", MessagePacket.TYPE, type));\n\n                }\n\n                break;\n\n            default:\n                Timber.e(\"Flow received unexpected response from client peer\");\n        }\n        return mIsComplete;\n    }\n\n    /**\n     * Called when data is received from the peer passed to this instance's constructor\n     * @return whether this flow is complete and should not receive further events.\n     */\n    public boolean onDataReceived(byte[] data) throws UnexpectedDataException {\n        // When data comes in we should be in a remote-peer writing state\n        if ((!mPeerIsHost && (mState == State.HOST_WRITE_ID || (mState == State.HOST_WRITE_MSGS && !mIsComplete))) ||\n            (mPeerIsHost && (mState == State.CLIENT_WRITE_ID || (mState == State.CLIENT_WRITE_MSGS && !mIsComplete)))) {\n\n            throw new IllegalStateException(String.format(\"onDataReceived invalid state %s for local as %s\", mState, mPeerIsHost ? \"client\" : \"host\"));\n\n        }\n\n        //Timber.d(\"Received data %s\", DataUtil.bytesToHex(data));\n\n        byte type = mProtocol.getPacketType(data);\n\n        switch (mState) {\n            case HOST_WRITE_ID:\n            case CLIENT_WRITE_ID:\n\n                switch(type) {\n                    case IdentityPacket.TYPE:\n\n                        mRemoteIdentity = mProtocol.deserializeIdentity(data);\n                        Timber.d(\"Got remote identity for %s\", mRemoteIdentity.alias);\n                        pro.dbro.ble.data.model.Peer remotePeer = mDataStore.createOrUpdateRemotePeerWithProtocolIdentity(mRemoteIdentity);\n                        // Only treat first identity as that of connected peer\n                        if (!mGotRemotePeerIdentity) {\n                            mCallback.onAppPeerStatusUpdated(this, remotePeer, Callback.ConnectionStatus.CONNECTED);\n                            mGotRemotePeerIdentity = true;\n                        }\n                        break;\n\n                    case NoDataPacket.TYPE:\n\n                        Timber.d(\"Received identity NoData\");\n                        incrementStateAndSendAsAppropriate();\n                        break;\n\n                    default:\n\n                        throw new UnexpectedDataException(String.format(\"Expected IdentityPacket (type %d). Got type %d\", IdentityPacket.TYPE, type));\n                }\n\n                break;\n\n            case HOST_WRITE_MSGS:\n            case CLIENT_WRITE_MSGS:\n\n                switch (type) {\n                    case MessagePacket.TYPE:\n\n                        MessagePacket msgPkt = mProtocol.deserializeMessageWithIdentity(data, mRemoteIdentity);\n                        Timber.d(\"Received msg %s\", msgPkt.body);\n\n                        // Mark incoming messages as delivered to sender\n\n                        boolean isNewMessage = true;\n                        Message existingMessage = mDataStore.getMessageBySignature(msgPkt.signature);\n                        if (existingMessage != null) {\n                            isNewMessage = false;\n                            existingMessage.close();\n                        }\n\n                        // TODO : Allow updating a message?\n                        Message msg = mDataStore.createOrUpdateMessageWithProtocolMessage(msgPkt);\n                        mDataStore.markMessageDeliveredToPeer(msgPkt, mRemoteIdentity);\n\n                        if (isNewMessage)\n                            mCallback.onMessageReceived(this, msg, mDataStore.getPeerByPubKey(mRemoteIdentity.publicKey));\n\n                        break;\n\n                    case NoDataPacket.TYPE:\n\n                        Timber.d(\"Received msg NoData\");\n                        incrementStateAndSendAsAppropriate();\n                        break;\n\n                    default:\n\n                        throw new UnexpectedDataException(String.format(\"Expected MessagePacket (type %d). Got type %d\", MessagePacket.TYPE, type));\n\n                }\n                break;\n\n            default:\n                Timber.e(\"Flow received unexpected response from client peer\");\n        }\n        return mIsComplete;\n    }\n\n    private void sendIdentity() {\n        if (!mFetchedIdentities) {\n\n            // If we're the client, we're initiating the identity flow, and we won't have the remote identity yet\n            mIdentityOutbox.addAll(getIdentitiesForIdentity(mRemoteIdentity == null ? null : mRemoteIdentity.publicKey,\n                    IDENTITIES_PER_RESPONSE));\n            mFetchedIdentities = true;\n        }\n\n        Timber.d(\"Send identity %s\", mIdentityOutbox.size() == 0 ? \"NoData\" : \"\");\n        mOutlet.sendData(mRemoteAirSharePeer,\n                         mIdentityOutbox.size() == 0 ?\n                            mProtocol.serializeNoDataPacket(mLocalIdentity).rawPacket :\n                            mIdentityOutbox.peek().rawPacket);\n    }\n\n    private void sendMessage() {\n        if (!mFetchedMessages) {\n            mMessageOutbox.addAll(getMessagesForIdentity(mRemoteIdentity.publicKey, MESSAGES_PER_RESPONSE));\n            mFetchedMessages = true;\n        }\n\n        Timber.d(\"Send message %s\", mMessageOutbox.size() == 0 ? \"NoData\" : \"\");\n        mOutlet.sendData(mRemoteAirSharePeer,\n                         mMessageOutbox.size() == 0 ?\n                            mProtocol.serializeNoDataPacket(mLocalIdentity).rawPacket :\n                            mMessageOutbox.peek().rawPacket);\n    }\n\n    private void incrementStateAndSendAsAppropriate() {\n        if (mState == State.HOST_WRITE_MSGS) {\n            Timber.d(\"ChatPeerFlow complete!\");\n            mIsComplete = true;\n            return;\n        }\n\n        mState = State.values()[mState.ordinal() + 1];\n        Timber.d(\"ChatPeerFlow New State : %s\", mState);\n        sendAsAppropriate();\n    }\n\n    private void sendAsAppropriate() {\n\n        switch (mState) {\n            case CLIENT_WRITE_ID:\n                if (mPeerIsHost) sendIdentity();\n                break;\n\n            case HOST_WRITE_ID:\n                if (!mPeerIsHost) sendIdentity();\n                break;\n\n            case CLIENT_WRITE_MSGS:\n                if (mPeerIsHost) sendMessage();\n                break;\n\n            case HOST_WRITE_MSGS:\n                if (!mPeerIsHost) sendMessage();\n                break;\n        }\n    }\n\n    /**\n     * Return a queue of message packets for delivery to remote identity with given public key.\n     *\n     * If recipientPublicKey is null, queues most recent messages\n     */\n    private ArrayDeque<MessagePacket> getMessagesForIdentity(@Nullable byte[] recipientPublicKey, int maxMessages) {\n        ArrayDeque<MessagePacket> messagePacketQueue = new ArrayDeque<>();\n\n        if (recipientPublicKey != null) {\n            // Get messages not delievered to peer\n            pro.dbro.ble.data.model.Peer recipient = mDataStore.getPeerByPubKey(recipientPublicKey);\n            List<MessagePacket> messages = mDataStore.getOutgoingMessagesForPeer(recipient, maxMessages);\n\n            if (messages == null || messages.size() == 0) {\n                Timber.d(\"Got no messages for peer with pub key \" + DataUtil.bytesToHex(recipientPublicKey));\n            } else {\n                messagePacketQueue.addAll(messages);\n            }\n        } else {\n            // Get most recent messages\n            MessageCollection recentMessages = mDataStore.getRecentMessages();\n            for (int x = 0; x < Math.min(maxMessages, recentMessages.getCursor().getCount()); x++) {\n                Message currentMessage = recentMessages.getMessageAtPosition(x);\n                if (currentMessage != null)\n                    messagePacketQueue.add(currentMessage.getProtocolMessage(mDataStore));\n            }\n            recentMessages.close();\n        }\n        return messagePacketQueue;\n    }\n\n    /**\n     * Return a queue of identity packets for delivery to the remote identity with the given\n     * public key.\n     *\n     * If recipientPublicKey is null, or no messages undelivered for recipient,\n     * the user identity will be queued. As such this method will never return a null\n     * or empty queue. Thus it should only be called once per flow and should not\n     * be used as an indication of whether identity transmission with a peer is complete.\n     */\n    private ArrayDeque<IdentityPacket> getIdentitiesForIdentity(@Nullable byte[] recipientPublicKey, int maxIdentities) {\n        List<IdentityPacket> identities = null;\n        ArrayDeque<IdentityPacket> identityPacketQueue = new ArrayDeque<>();\n        if (recipientPublicKey != null) {\n            // We have a public key for the remote peer, fetch undelivered identities\n            pro.dbro.ble.data.model.Peer recipient = mDataStore.getPeerByPubKey(recipientPublicKey);\n            identities = mDataStore.getOutgoingIdentitiesForPeer(recipient, maxIdentities);\n        }\n\n        if (identities == null || identities.size() == 0) {\n            Timber.d(\"Got no identities to send for peer %s. Sending own identity\", recipientPublicKey == null ? \"\" : \"with pub key \" + DataUtil.bytesToHex(recipientPublicKey).substring(2, 6));\n            // For now, at least send our identity\n            if (identities == null) identities = new ArrayList<>(1);\n            identities.add(mDataStore.getPrimaryLocalPeer().getIdentity());\n        }\n        identityPacketQueue.addAll(identities);\n\n        return identityPacketQueue;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/PrefsManager.java",
    "content": "package pro.dbro.ble;\n\nimport android.content.Context;\n\n\n/**\n * Created by davidbrodsky on 9/21/14.\n */\npublic class PrefsManager {\n\n    /** SharedPreferences store names */\n    private static final String APP_PREFS = \"prefs\";\n\n    /** SharedPreferences keys */\n    private static final String APP_STATUS = \"status\";\n\n    public static int getStatus(Context context) {\n        return context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE)\n                      .getInt(APP_STATUS, 0);\n    }\n\n    public static void setStatus(Context context, int status) {\n        context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE).edit()\n               .putInt(APP_STATUS, status)\n               .commit();\n    }\n\n    public static void clearState(Context context) {\n        context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE).edit().clear().apply();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/crypto/KeyPair.java",
    "content": "package pro.dbro.ble.crypto;\n\n/**\n * Created by davidbrodsky on 10/22/14.\n */\npublic class KeyPair {\n\n    public final byte[] publicKey;\n    public final byte[] secretKey;\n\n    public KeyPair(byte[] publicKey, byte[] secretKey) {\n        this.publicKey = publicKey;\n        this.secretKey = secretKey;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/crypto/SodiumShaker.java",
    "content": "package pro.dbro.ble.crypto;\n\nimport android.support.annotation.NonNull;\n\nimport org.abstractj.kalium.NaCl;\nimport org.abstractj.kalium.Sodium;\n\n/**\n * Wrapper around libsodium functions.\n *\n * Created by davidbrodsky on 10/13/14.\n */\npublic class SodiumShaker {\n    private static final String TAG = \"Identity\";\n\n    public static final int crypto_sign_PUBLICKEYBYTES = 32;\n    private static final int crypto_sign_SECRETKEYBYTES = 64;\n    public static final int crypto_sign_BYTES = 64;\n\n    static {\n        // Load native libraries\n        NaCl.sodium();\n        // Initialize libsodium\n        if (Sodium.sodium_init() == -1) {\n            throw new IllegalStateException(\"sodiun_init failed!\");\n        }\n    }\n\n    public static KeyPair generateKeyPair() {\n        byte[] pk = new byte[crypto_sign_PUBLICKEYBYTES];\n        byte[] sk = new byte[crypto_sign_SECRETKEYBYTES];\n\n        Sodium.crypto_sign_ed25519_keypair(pk, sk);\n        return new KeyPair(pk, sk);\n    }\n\n    public static byte[] generateSignatureForMessage(@NonNull byte[] secret_key, @NonNull byte[] message, int message_len) {\n        if (secret_key.length != crypto_sign_SECRETKEYBYTES) throw new IllegalArgumentException(\"secret_key is incorrect length\");\n        byte[] signature = new byte[crypto_sign_BYTES];\n        int[] signature_len = new int[0];\n\n        Sodium.crypto_sign_ed25519_detached(signature, signature_len, message, message_len, secret_key);\n\n        return signature;\n    }\n\n    /**\n     * Very that signature and public_key verify message\n     *\n     * @param public_key the public key corresponding to signature\n     * @param signature the signature of message decipherable with public_key\n     * @param message the data with signature\n     */\n    public static boolean verifySignature(@NonNull byte[] public_key, @NonNull byte[] signature, @NonNull byte[] message) {\n        // Verify signature\n\n        if (Sodium.crypto_sign_ed25519_verify_detached(signature, message, message.length, public_key) != 0) {\n            /* Incorrect signature! */\n            return false;\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/ContentProviderStore.java",
    "content": "package pro.dbro.ble.data;\n\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.net.Uri;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.util.Log;\n\nimport java.util.ArrayList;\nimport java.util.Date;\nimport java.util.List;\n\nimport pro.dbro.ble.crypto.KeyPair;\nimport pro.dbro.ble.crypto.SodiumShaker;\nimport pro.dbro.ble.data.model.ChatContentProvider;\nimport pro.dbro.ble.data.model.DataUtil;\nimport pro.dbro.ble.data.model.IdentityDeliveryTable;\nimport pro.dbro.ble.data.model.Message;\nimport pro.dbro.ble.data.model.MessageCollection;\nimport pro.dbro.ble.data.model.MessageDeliveryTable;\nimport pro.dbro.ble.data.model.MessageTable;\nimport pro.dbro.ble.data.model.Peer;\nimport pro.dbro.ble.data.model.PeerTable;\nimport pro.dbro.ble.protocol.IdentityPacket;\nimport pro.dbro.ble.protocol.MessagePacket;\nimport pro.dbro.ble.protocol.OwnedIdentityPacket;\nimport pro.dbro.ble.protocol.Protocol;\n\n/**\n * API for the application's data persistence\n *\n * If the underlying data storage were to be replaced, this should be the\n * only class requiring modification.\n *\n * Created by davidbrodsky on 10/20/14.\n */\npublic class ContentProviderStore extends DataStore {\n    public static final String TAG = \"DataManager\";\n\n    public ContentProviderStore(Context context) {\n        super(context);\n    }\n\n    @Override\n    public void markMessageDeliveredToPeer(@NonNull MessagePacket messagePacket, @NonNull IdentityPacket recipientPacket) {\n        Message message = getMessageBySignature(messagePacket.signature);\n        Peer recipient = getPeerByPubKey(recipientPacket.publicKey);\n\n        if (message == null || recipient == null) {\n            Log.w(TAG, \"Unable to record message delivery. No peer or message database id available\");\n            return;\n        }\n\n        ContentValues delivery = new ContentValues();\n        delivery.put(MessageDeliveryTable.messageId, message.getId());\n        delivery.put(MessageDeliveryTable.peerId, recipient.getId());\n\n        mContext.getContentResolver().insert(ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES, delivery);\n        Log.i(TAG, \"Recorded message delivery\");\n        message.close();\n    }\n\n    @Override\n    public void markIdentityDeliveredToPeer(@NonNull IdentityPacket payloadIdentity, @NonNull IdentityPacket recipientIdentity) {\n        Peer payloadPeer = getPeerByPubKey(payloadIdentity.publicKey);\n        Peer recipientPeer = getPeerByPubKey(recipientIdentity.publicKey);\n\n        if (payloadPeer == null || recipientPeer == null) {\n            Log.w(TAG, \"Unable to fetch payload or recipient identity. Cannot mark identity delivered\");\n            return;\n        }\n\n        ContentValues delivery = new ContentValues();\n        delivery.put(IdentityDeliveryTable.peerPayloadId, payloadPeer.getId());\n        delivery.put(IdentityDeliveryTable.peerRecipientId, recipientPeer.getId());\n\n        mContext.getContentResolver().insert(ChatContentProvider.IdentityDeliveries.IDENTITY_DELIVERIES, delivery);\n        Log.i(TAG, \"Recorded identity delivery\");\n    }\n\n    @Nullable\n    @Override\n    public Peer createLocalPeerWithAlias(@NonNull String alias, @Nullable Protocol protocol) {\n        KeyPair keyPair = SodiumShaker.generateKeyPair();\n        ContentValues dbEntry = new ContentValues();\n        dbEntry.put(PeerTable.pubKey, keyPair.publicKey);\n        dbEntry.put(PeerTable.secKey, keyPair.secretKey);\n        dbEntry.put(PeerTable.alias, alias);\n        dbEntry.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date()));\n        if (protocol != null) {\n            // If protocol is available, use it to cache the Identity packet for transmission\n            dbEntry.put(PeerTable.rawPkt, protocol.serializeIdentity(\n                    new OwnedIdentityPacket(keyPair.secretKey, keyPair.publicKey, alias, null)));\n        }\n        Uri newIdentityUri = mContext.getContentResolver().insert(ChatContentProvider.Peers.PEERS, dbEntry);\n        return getPeerById(Integer.parseInt(newIdentityUri.getLastPathSegment()));\n    }\n\n    /**\n     * @return the first user peer entry in the database,\n     * or null if no identity is set.\n     */\n    @Override\n    @Nullable\n    public Peer getPrimaryLocalPeer() {\n        // TODO: caching\n        Cursor result = mContext.getContentResolver().query(ChatContentProvider.Peers.PEERS,\n                null,\n                PeerTable.secKey + \" IS NOT NULL\",\n                null,\n                null);\n        if (result != null && result.moveToFirst()) {\n            Peer peer = new Peer(result);\n            result.close();\n            return peer;\n        }\n        return null;\n    }\n\n    @Nullable\n    @Override\n    public List<MessagePacket> getOutgoingMessagesForPeer(@NonNull Peer recipient, int maxMessages) {\n        // TODO : Don't send messages past a certain age etc?\n        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, null, null, null, null);\n        if (messagesCursor != null) {\n            List<MessagePacket> messagesToSend = new ArrayList<>();\n            while (messagesCursor.moveToNext()) {\n                Message individualMessage = new Message(messagesCursor);\n                if (!haveDeliveredMessageToPeer(individualMessage, recipient)) {\n                    messagesToSend.add(individualMessage.getProtocolMessage(this));\n                    if (messagesToSend.size() > maxMessages) break;\n                }\n            }\n\n            messagesCursor.close();\n            return messagesToSend;\n        }\n        return null;\n    }\n\n    @Override\n    public List<IdentityPacket> getOutgoingIdentitiesForPeer(@NonNull Peer recipient, int maxIdentities) {\n        // TODO : Don't send identities past a certain age etc?\n        Cursor identitiesCursor = mContext.getContentResolver().query(ChatContentProvider.Peers.PEERS, null, null, null, null);\n        if (identitiesCursor != null) {\n            List<IdentityPacket> identitiesToSend = new ArrayList<>();\n            while (identitiesCursor.moveToNext()) {\n                Peer payloadPeer = new Peer(identitiesCursor);\n                if (!haveDeliveredPeerIdentityToPeer(payloadPeer, recipient)) {\n                    identitiesToSend.add(payloadPeer.getIdentity());\n                    if (identitiesToSend.size() > maxIdentities) break;\n                }\n\n            }\n\n            identitiesCursor.close();\n            return identitiesToSend;\n        }\n        return null;\n    }\n\n    @Override\n    public MessageCollection getRecentMessages() {\n        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES,\n                null,\n                null,\n                null,\n                MessageTable.receivedDate + \" DESC\");\n\n        if (messagesCursor != null /*&& messagesCursor.moveToFirst()*/) {\n            return new MessageCollection(messagesCursor);\n        }\n        return null;\n    }\n\n    @Override\n    public MessageCollection getRecentMessagesByPeer(@NonNull Peer author) {\n        Cursor messagesCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES,\n                null,\n                MessageTable.peerId + \"=?\",\n                new String[] { String.valueOf(author.getId()) },\n                MessageTable.receivedDate + \" DESC\");\n\n        if (messagesCursor != null /*&& messagesCursor.moveToFirst()*/) {\n            return new MessageCollection(messagesCursor);\n        }\n        return null;\n    }\n\n    @Nullable\n    @Override\n    public Peer createOrUpdateRemotePeerWithProtocolIdentity(@NonNull IdentityPacket remoteIdentityPacket) {\n        // Query if peer exists\n        Peer peer = getPeerByPubKey(remoteIdentityPacket.publicKey);\n\n        ContentValues peerValues = new ContentValues();\n        peerValues.put(PeerTable.lastSeenDate, DataUtil.storedDateFormatter.format(new Date()));\n        peerValues.put(PeerTable.pubKey, remoteIdentityPacket.publicKey);\n        peerValues.put(PeerTable.alias, remoteIdentityPacket.alias);\n        peerValues.put(PeerTable.rawPkt, remoteIdentityPacket.rawPacket);\n\n        if (peer != null) {\n            // Peer exists. Modify lastSeenDate\n            Log.i(TAG, \"Updating peer for pubkey \" + DataUtil.bytesToHex(remoteIdentityPacket.publicKey));\n\n            int updated = mContext.getContentResolver().update(\n                    ChatContentProvider.Peers.PEERS,\n                    peerValues,\n                    \"quote(\"+ PeerTable.pubKey + \") = ?\" ,\n                    new String[] {DataUtil.bytesToHex(remoteIdentityPacket.publicKey)});\n            if (updated != 1) {\n                Log.e(TAG, \"Failed to update peer last seen\");\n            }\n        } else {\n            // Peer does not exist. Create.\n            Uri peerUri = mContext.getContentResolver().insert(\n                    ChatContentProvider.Peers.PEERS,\n                    peerValues);\n\n            // Fetch newly created peer\n            peer = getPeerById(Integer.parseInt(peerUri.getLastPathSegment()));\n            Log.i(TAG, String.format(\"Created new peer %d for pubkey %s\", Integer.parseInt(peerUri.getLastPathSegment()), DataUtil.bytesToHex(remoteIdentityPacket.publicKey)));\n\n            if (peer == null) {\n                Log.e(TAG, \"Failed to query peer after insertion.\");\n            }\n        }\n        return peer;\n    }\n\n    @Nullable\n    @Override\n    public Message createOrUpdateMessageWithProtocolMessage(@NonNull MessagePacket protocolMessagePacket) {\n        // Query if peer exists\n        Peer peer = getPeerByPubKey(protocolMessagePacket.sender.publicKey);\n\n        if (peer == null)\n            throw new IllegalStateException(\"Failed to get peer for message\");\n\n        // See if message exists\n        Message message = getMessageBySignature(protocolMessagePacket.signature);\n        if (message == null) {\n            // Message doesn't exist in our database\n\n            // Insert message into database\n            ContentValues newMessageEntry = new ContentValues();\n            newMessageEntry.put(MessageTable.body, protocolMessagePacket.body);\n            newMessageEntry.put(MessageTable.peerId, peer.getId());\n            newMessageEntry.put(MessageTable.receivedDate, DataUtil.storedDateFormatter.format(new Date()));\n            newMessageEntry.put(MessageTable.authoredDate, DataUtil.storedDateFormatter.format(protocolMessagePacket.authoredDate));\n            newMessageEntry.put(MessageTable.signature, protocolMessagePacket.signature);\n            newMessageEntry.put(MessageTable.replySig, protocolMessagePacket.replySig);\n            newMessageEntry.put(MessageTable.rawPacket, protocolMessagePacket.rawPacket);\n\n            Uri newMessageUri = mContext.getContentResolver().insert(\n                    ChatContentProvider.Messages.MESSAGES,\n                    newMessageEntry);\n            message = getMessageById(Integer.parseInt(newMessageUri.getLastPathSegment()));\n        } else {\n            // We already have a message with this signature\n            // Since we currently don't have any mutable message fields (e.g hopcount)\n            // do nothing\n            Log.i(TAG, \"Received stored message. Ignoring\");\n        }\n\n        return message;\n    }\n\n    @Nullable\n    @Override\n    public Message getMessageBySignature(@NonNull byte[] signature) {\n        Cursor messageCursor = mContext.getContentResolver().query(\n                ChatContentProvider.Messages.MESSAGES,\n                null,\n                \"quote(\" + MessageTable.signature + \") = ?\",\n                new String[] {DataUtil.bytesToHex(signature)},\n                null);\n        if (messageCursor != null && messageCursor.moveToFirst()) {\n            return new Message(messageCursor);\n        }\n        return null;\n    }\n\n    @Nullable\n    @Override\n    public Message getMessageById(int id) {\n        Cursor messageCursor = mContext.getContentResolver().query(ChatContentProvider.Messages.MESSAGES, null,\n                MessageTable.id + \" = ?\",\n                new String[]{String.valueOf(id)},\n                null);\n        if (messageCursor != null && messageCursor.moveToFirst()) {\n            return new Message(messageCursor);\n        }\n        return null;\n    }\n\n    @Nullable\n    @Override\n    public Peer getPeerByPubKey(@NonNull byte[] publicKey) {\n        Cursor peerCursor = mContext.getContentResolver().query(\n                ChatContentProvider.Peers.PEERS,\n                null,\n                \"quote(\" + PeerTable.pubKey + \") = ?\",\n                new String[] {DataUtil.bytesToHex(publicKey)},\n                null);\n        if (peerCursor != null && peerCursor.moveToFirst()) {\n            Peer peer = new Peer(peerCursor);\n            peerCursor.close();\n            return peer;\n        }\n        return null;\n    }\n\n    @Nullable\n    @Override\n    public Peer getPeerById(int id) {\n        Cursor peerCursor = mContext.getContentResolver().query(\n                ChatContentProvider.Peers.PEERS,\n                null,\n                PeerTable.id + \" = ?\",\n                new String[] {String.valueOf(id)},\n                null);\n        if (peerCursor != null && peerCursor.moveToFirst()) {\n            Peer peer = new Peer(peerCursor);\n            peerCursor.close();\n            return peer;\n        }\n        return null;\n    }\n\n    @Override\n    public int countPeers() {\n        Cursor peerCursor = mContext.getContentResolver().query(\n                ChatContentProvider.Peers.PEERS,\n                new String[] {PeerTable.id},\n                null,\n                null,\n                null);\n        if (peerCursor != null) {\n            int result = peerCursor.getCount();\n            peerCursor.close();\n            return result;\n        }\n        return 0;\n    }\n\n    @Override\n    public int countMessagesPassed() {\n        Cursor deliveryCursor = mContext.getContentResolver().query(\n                ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES,\n                new String[] {MessageDeliveryTable.id},\n                null,\n                null,\n                null);\n        if (deliveryCursor != null) {\n            int result = deliveryCursor.getCount();\n            deliveryCursor.close();\n            return result;\n        }\n        return 0;\n    }\n\n    /** Utility */\n\n    private boolean haveDeliveredMessageToPeer(Message message, Peer peer) {\n        Cursor deliveryCursor = mContext.getContentResolver().query(ChatContentProvider.MessageDeliveries.MESSAGE_DELIVERIES,\n                null,\n                MessageDeliveryTable.messageId + \" = ? AND \" + MessageDeliveryTable.peerId + \" = ?\",\n                new String[]{String.valueOf(message.getId()), String.valueOf(peer.getId())},\n                null);\n        try {\n            return deliveryCursor != null && deliveryCursor.moveToFirst();\n        } finally {\n            if (deliveryCursor != null) deliveryCursor.close();\n        }\n    }\n\n    /**\n     * @return whether peerPayload has been delivered to peerRecipient\n     */\n    private boolean haveDeliveredPeerIdentityToPeer(Peer peerPayload, Peer peerRecipient) {\n        Cursor deliveryCursor = mContext.getContentResolver().query(ChatContentProvider.IdentityDeliveries.IDENTITY_DELIVERIES,\n                null,\n                IdentityDeliveryTable.peerRecipientId + \" = ? AND \" + IdentityDeliveryTable.peerPayloadId + \" = ?\",\n                new String[]{String.valueOf(peerRecipient.getId()), String.valueOf(peerPayload.getId())},\n                null);\n        try {\n            return deliveryCursor != null && deliveryCursor.moveToFirst();\n        } finally {\n            if (deliveryCursor != null) deliveryCursor.close();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/DataStore.java",
    "content": "package pro.dbro.ble.data;\n\nimport android.content.Context;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\nimport java.util.List;\n\nimport pro.dbro.ble.data.model.Message;\nimport pro.dbro.ble.data.model.MessageCollection;\nimport pro.dbro.ble.data.model.Peer;\nimport pro.dbro.ble.protocol.IdentityPacket;\nimport pro.dbro.ble.protocol.MessagePacket;\nimport pro.dbro.ble.protocol.Protocol;\n\n/**\n * Data persistence layer. Any data storage mechanism\n * needs to implement this interface.\n *\n * Created by davidbrodsky on 10/20/14.\n */\npublic abstract class DataStore {\n\n    protected Context mContext;\n\n    public DataStore(@NonNull Context context) {\n        mContext = context.getApplicationContext();\n    }\n\n    public abstract void markMessageDeliveredToPeer(@NonNull MessagePacket message, @NonNull IdentityPacket recipient);\n\n    public abstract void markIdentityDeliveredToPeer(@NonNull IdentityPacket payloadIdentity, @NonNull IdentityPacket recipientIdentity);\n\n    public abstract Peer createLocalPeerWithAlias(@NonNull String alias, @Nullable Protocol protocol);\n\n    public abstract Peer getPrimaryLocalPeer();\n\n    public abstract List<MessagePacket> getOutgoingMessagesForPeer(@NonNull Peer recipient, int maxMessages);\n\n    public abstract List<IdentityPacket> getOutgoingIdentitiesForPeer(@NonNull Peer recipient, int maxMessages);\n\n    public abstract MessageCollection getRecentMessages();\n\n    public abstract MessageCollection getRecentMessagesByPeer(@NonNull Peer author);\n\n    public abstract Peer createOrUpdateRemotePeerWithProtocolIdentity(@NonNull IdentityPacket identityPacket);\n\n    public abstract Message createOrUpdateMessageWithProtocolMessage(@NonNull MessagePacket protocolMessagePacket);\n\n    public abstract Message getMessageBySignature(@NonNull byte[] signature);\n\n    public abstract Message getMessageById(int id);\n\n    public abstract Peer getPeerByPubKey(@NonNull byte[] publicKey);\n\n    public abstract Peer getPeerById(int id);\n\n    public abstract int countPeers();\n\n    public abstract int countMessagesPassed();\n\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/ChatContentProvider.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport android.net.Uri;\n\nimport net.simonvt.schematic.annotation.ContentProvider;\nimport net.simonvt.schematic.annotation.ContentUri;\nimport net.simonvt.schematic.annotation.TableEndpoint;\n\n/**\n * ContentProvider definition. This defines a familiar API\n * for Android framework components to utilize.\n *\n * Created by davidbrodsky on 7/28/14.\n */\n@ContentProvider(authority = ChatContentProvider.AUTHORITY, database = ChatDatabase.class)\npublic final class ChatContentProvider {\n\n    public static final String AUTHORITY      = \"pro.dbro.ble.chatprovider\";\n    private static final Uri BASE_CONTENT_URI = Uri.parse(\"content://\" + AUTHORITY);\n\n    private static Uri buildUri(String... paths) {\n        Uri.Builder builder = BASE_CONTENT_URI.buildUpon();\n        for (String path : paths) {\n            builder.appendPath(path);\n        }\n        return builder.build();\n    }\n\n    /** Peer API **/\n\n    @TableEndpoint(table = ChatDatabase.PEERS)\n    public static class Peers {\n\n        private static final String ENDPOINT = \"peers\";\n\n        @ContentUri(\n                path = ENDPOINT,\n                type = \"vnd.android.cursor.dir/list\",\n                defaultSort = PeerTable.alias + \" ASC\")\n        public static final Uri PEERS = buildUri(ENDPOINT);\n    }\n\n    /** Messages API **/\n\n    @TableEndpoint(table = ChatDatabase.MESSAGES)\n    public static class Messages {\n\n        private static final String ENDPOINT = \"msgs\";\n\n        @ContentUri(\n                path = ENDPOINT,\n                type = \"vnd.android.cursor.dir/list\",\n                defaultSort = MessageTable.authoredDate + \" ASC\")\n        public static final Uri MESSAGES = buildUri(ENDPOINT);\n\n    }\n\n    /** MessageDelivery API **/\n\n    @TableEndpoint(table = ChatDatabase.DELIVERED_MESSAGES)\n    public static class MessageDeliveries {\n\n        private static final String ENDPOINT = \"message_deliveries\";\n\n        @ContentUri(\n                path = ENDPOINT,\n                type = \"vnd.android.cursor.dir/list\",\n                defaultSort = MessageDeliveryTable.messageId + \" ASC\")\n        public static final Uri MESSAGE_DELIVERIES = buildUri(ENDPOINT);\n\n    }\n\n    /** IdentityDelivery API **/\n\n    @TableEndpoint(table = ChatDatabase.DELIVERED_IDENTITIES)\n    public static class IdentityDeliveries {\n\n        private static final String ENDPOINT = \"identity_deliveries\";\n\n        @ContentUri(\n                path = ENDPOINT,\n                type = \"vnd.android.cursor.dir/list\",\n                defaultSort = IdentityDeliveryTable.peerRecipientId + \" ASC\")\n        public static final Uri IDENTITY_DELIVERIES = buildUri(ENDPOINT);\n\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/ChatDatabase.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.Database;\nimport net.simonvt.schematic.annotation.Table;\n\n/**\n * SQL Database definition.\n *\n * Created by davidbrodsky on 7/28/14.\n */\n@Database(version = ChatDatabase.DATABASE_VERSION)\npublic class ChatDatabase {\n\n    public static final int DATABASE_VERSION = 1;\n\n    /** Table Definition                Reference Name                                     SQL Tablename */\n    @Table(PeerTable.class)             public static final String  PEERS                = \"peers\";\n    @Table(MessageTable.class)          public static final String  MESSAGES             = \"msgs\";\n    @Table(MessageDeliveryTable.class)  public static final String  DELIVERED_MESSAGES   = \"m_dlvry\";\n    @Table(IdentityDeliveryTable.class) public static final String  DELIVERED_IDENTITIES = \"p_dlvry\";\n}"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/CursorModel.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\n\nimport java.io.Closeable;\n\n/**\n * Created by davidbrodsky on 10/20/14.\n */\npublic abstract class CursorModel implements Closeable{\n\n    protected Cursor mCursor;\n\n    /**\n     * Use this constructor if you intend to immediately access model data.\n     * @param cursor A cursor that is already moved to the row corresponding to the desired model instance\n     */\n    public CursorModel(@NonNull Cursor cursor) {\n        mCursor = cursor;\n    }\n\n    public Cursor getCursor() {\n        return mCursor;\n    }\n\n    @Override\n    public void close() {\n        if (mCursor != null) {\n            mCursor.close();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/DataUtil.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport java.text.SimpleDateFormat;\nimport java.util.Locale;\n\n/**\n * Utilities for converting between Java and Database friendly types\n *\n * Created by davidbrodsky on 10/13/14.\n */\npublic class DataUtil {\n\n    public static SimpleDateFormat storedDateFormatter = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\", Locale.US);\n\n    final protected static char[] hexArray = \"0123456789ABCDEF\".toCharArray();\n\n    /**\n     * When we query rows by a BLOB column we must\n     * convert the BLOB to its String hex form\n     * see:\n     * http://www.sqlite.org/lang_expr.html#litvalue\n     */\n    public static String bytesToHex(byte[] bytes) {\n        char[] hexChars = new char[bytes.length * 2];\n        for ( int j = 0; j < bytes.length; j++ ) {\n            int v = bytes[j] & 0xFF;\n            hexChars[j * 2] = hexArray[v >>> 4];\n            hexChars[j * 2 + 1] = hexArray[v & 0x0F];\n        }\n        String rawHex = new String(hexChars);\n        String blobLiteral = \"X'\" + rawHex + \"'\";\n        return blobLiteral;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/IdentityDeliveryTable.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.AutoIncrement;\nimport net.simonvt.schematic.annotation.DataType;\nimport net.simonvt.schematic.annotation.NotNull;\nimport net.simonvt.schematic.annotation.PrimaryKey;\n\nimport static net.simonvt.schematic.annotation.DataType.Type.INTEGER;\n\n/**\n * Used to avoid sending a single identity to a particular client multiple times\n *\n * Created by davidbrodsky on 7/28/14.\n */\npublic interface IdentityDeliveryTable {\n\n    /** SQL type        Modifiers                   Reference Name            SQL Column Name */\n    @DataType(INTEGER)  @PrimaryKey @AutoIncrement  String id                  = \"_id\";\n    @DataType(INTEGER)  @NotNull                    String peerRecipientId     = \"pr_id\";\n    @DataType(INTEGER)  @NotNull                    String peerPayloadId       = \"pp_id\";\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/Message.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\nimport java.text.ParseException;\nimport java.util.Date;\n\nimport pro.dbro.ble.data.DataStore;\nimport pro.dbro.ble.protocol.MessagePacket;\n\n/**\n * A thin model around a {@link android.database.Cursor}\n * that lazy-loads attributes as needed. As such, do\n * not to close the cursor fed to this class's constructor.\n * Instead call {@link #close}\n * <p/>\n * Created by davidbrodsky on 10/12/14.\n */\npublic class Message extends CursorModel {\n\n    public Message(@NonNull Cursor cursor) {\n        super(cursor);\n\n    }\n\n    public int getId() {\n        return mCursor.getInt(mCursor.getColumnIndex(MessageTable.id));\n    }\n\n    public String getBody() {\n        return mCursor.getString(mCursor.getColumnIndex(MessageTable.body));\n    }\n\n    public Date getAuthoredDate() {\n        try {\n            return DataUtil.storedDateFormatter.parse(mCursor.getString(mCursor.getColumnIndex(MessageTable.authoredDate)));\n        } catch (ParseException e) {\n            e.printStackTrace();\n            return null;\n        }\n    }\n\n    public byte[] getPublicKey(DataStore dataStore) {\n        return getSender(dataStore).getIdentity().publicKey;\n    }\n\n    public byte[] getSignature() {\n        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.signature));\n    }\n\n    public byte[] getReplySignature() {\n        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.replySig));\n    }\n\n    public byte[] getRawPacket() {\n        return mCursor.getBlob(mCursor.getColumnIndex(MessageTable.rawPacket));\n    }\n\n    @Nullable\n    public Peer getSender(DataStore dataStore) {\n        return dataStore.getPeerById(mCursor.getInt(mCursor.getColumnIndex(MessageTable.peerId)));\n    }\n\n    @Nullable\n    public MessagePacket getProtocolMessage(DataStore dataStore) {\n        return new MessagePacket(\n                getSender(dataStore).getIdentity(),\n                getSignature(),\n                getReplySignature(),\n                getBody(),\n                getRawPacket(),\n                getAuthoredDate());\n\n    }\n\n    @Nullable\n    public Date getRelativeReceivedDate() {\n        try {\n            return DataUtil.storedDateFormatter.parse(\n                    mCursor.getString(mCursor.getColumnIndex(MessageTable.authoredDate)));\n        } catch (ParseException e) {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/MessageCollection.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\n\n/**\n * Created by davidbrodsky on 10/20/14.\n */\npublic class MessageCollection extends CursorModel {\n\n    public MessageCollection(@NonNull Cursor cursor) {\n        super(cursor);\n    }\n\n    @Nullable\n    public Message getMessageAtPosition(int position) {\n        boolean success = mCursor.move(position);\n        if (success)\n            return new Message(mCursor);\n        return null;\n    }\n\n    public Cursor getCursor() {\n        return mCursor;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/MessageDeliveryTable.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.AutoIncrement;\nimport net.simonvt.schematic.annotation.DataType;\nimport net.simonvt.schematic.annotation.NotNull;\nimport net.simonvt.schematic.annotation.PrimaryKey;\n\nimport static net.simonvt.schematic.annotation.DataType.Type.INTEGER;\n\n/**\n * Used to avoid sending a single messages to a particular client multiple times\n *\n * Created by davidbrodsky on 7/28/14.\n */\npublic interface MessageDeliveryTable {\n\n    /** SQL type        Modifiers                   Reference Name            SQL Column Name */\n    @DataType(INTEGER)  @PrimaryKey @AutoIncrement  String id                  = \"_id\";\n    @DataType(INTEGER)  @NotNull                    String messageId           = \"m_id\";\n    @DataType(INTEGER)  @NotNull                    String peerId              = \"p_id\";\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/MessageTable.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.AutoIncrement;\nimport net.simonvt.schematic.annotation.DataType;\nimport net.simonvt.schematic.annotation.NotNull;\nimport net.simonvt.schematic.annotation.PrimaryKey;\n\nimport static net.simonvt.schematic.annotation.DataType.Type.BLOB;\nimport static net.simonvt.schematic.annotation.DataType.Type.INTEGER;\nimport static net.simonvt.schematic.annotation.DataType.Type.TEXT;\n\n/**\n * Created by davidbrodsky on 7/28/14.\n */\npublic interface MessageTable {\n\n    /** SQL type        Modifiers                   Reference Name            SQL Column Name */\n    @DataType(INTEGER)  @PrimaryKey @AutoIncrement  String id               = \"_id\";\n    @DataType(TEXT)     @NotNull                    String body             = \"body\";\n    @DataType(INTEGER)                              String peerId           = \"p_id\";\n    @DataType(TEXT)                                 String authoredDate     = \"author_date\";\n    @DataType(TEXT)                                 String receivedDate     = \"recv_date\";\n    @DataType(BLOB)                                 String signature        = \"sig\";\n    @DataType(BLOB)                                 String replySig         = \"r_sig\";\n    @DataType(BLOB)                                 String rawPacket        = \"pkt\";\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/Peer.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\nimport java.text.ParseException;\nimport java.util.Date;\n\nimport pro.dbro.ble.protocol.IdentityPacket;\nimport pro.dbro.ble.protocol.OwnedIdentityPacket;\n\n/**\n * Created by davidbrodsky on 10/12/14.\n */\npublic class Peer {\n\n    private int mId;\n    private byte[] mPublicKey;\n    private byte[] mSecretKey;\n    private String mAlias;\n    private Date mLastSeen;\n\n    private byte[] mRawPkt;\n\n\n    public Peer(@NonNull Cursor cursor) {\n        mId = cursor.getInt(cursor.getColumnIndex(PeerTable.id));\n        mPublicKey = cursor.getBlob(cursor.getColumnIndex(PeerTable.pubKey));\n        mSecretKey = cursor.getBlob(cursor.getColumnIndex(PeerTable.secKey));\n        mAlias = cursor.getString(cursor.getColumnIndex(PeerTable.alias));\n        mRawPkt = cursor.getBlob(cursor.getColumnIndex(PeerTable.rawPkt));\n\n        try {\n            mLastSeen = DataUtil.storedDateFormatter.parse(cursor.getString(cursor.getColumnIndex(PeerTable.lastSeenDate)));\n        } catch (ParseException e) {\n            e.printStackTrace();\n        }\n    }\n\n    public int getId() {\n       return mId;\n    }\n\n    public byte[] getPublicKey() {\n        return mPublicKey;\n    }\n\n    public String getAlias() {\n        return mAlias;\n    }\n\n    @Nullable\n    public Date getLastDateSeen() {\n        return mLastSeen;\n    }\n    /**\n     * @return whether this peer represents the application user.\n     * e.g: Do we have a secret key\n     */\n    public boolean isLocalPeer() {\n        return mSecretKey != null && mSecretKey.length > 0;\n    }\n\n    /**\n     * @return a {@link pro.dbro.ble.protocol.OwnedIdentityPacket} for this peer,\n     * or an {@link pro.dbro.ble.protocol.IdentityPacket} if this peer is not a user-owned peer.\n     * <p/>\n     * see {@link #isLocalPeer()}\n     */\n    public IdentityPacket getIdentity() {\n        if (!isLocalPeer()) {\n            return new IdentityPacket(mPublicKey, mAlias, mLastSeen, mRawPkt);\n        } else {\n            return new OwnedIdentityPacket(mSecretKey, mPublicKey, mAlias, mRawPkt);\n        }\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n\n        if(obj == this) return true;\n        if(obj == null) return false;\n\n        if (getClass().equals(obj.getClass()))\n        {\n            final Peer other = (Peer) obj;\n\n            return mId == other.mId;\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/data/model/PeerTable.java",
    "content": "package pro.dbro.ble.data.model;\n\nimport net.simonvt.schematic.annotation.AutoIncrement;\nimport net.simonvt.schematic.annotation.DataType;\nimport net.simonvt.schematic.annotation.NotNull;\nimport net.simonvt.schematic.annotation.PrimaryKey;\n\nimport static net.simonvt.schematic.annotation.DataType.Type.BLOB;\nimport static net.simonvt.schematic.annotation.DataType.Type.INTEGER;\nimport static net.simonvt.schematic.annotation.DataType.Type.TEXT;\n\n/**\n * Created by davidbrodsky on 7/28/14.\n */\npublic interface PeerTable {\n\n    /** SQL type        Modifiers                   Reference Name            SQL Column Name */\n    @DataType(INTEGER)  @PrimaryKey @AutoIncrement  String id               = \"_id\";\n    @DataType(TEXT)                                 String alias            = \"alias\";\n    @DataType(TEXT)     @NotNull                    String lastSeenDate     = \"last_seen\";\n    @DataType(BLOB)     @NotNull                    String pubKey           = \"pk\";\n    @DataType(BLOB)                                 String secKey           = \"sk\";\n    @DataType(BLOB)                                 String rawPkt           = \"pkt\";\n\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/BLEProtocol.java",
    "content": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.util.Log;\n\nimport java.io.UnsupportedEncodingException;\nimport java.nio.ByteBuffer;\nimport java.nio.ByteOrder;\nimport java.util.Date;\n\nimport pro.dbro.ble.crypto.SodiumShaker;\n\n/**\n * Created by davidbrodsky on 10/14/14.\n */\npublic class BLEProtocol implements Protocol {\n    public static final String TAG = \"ChatProtocol\";\n\n    // <editor-fold desc=\"Public API\">\n    /** Bluetooth LE Mesh Chat Protocol Version */\n    public static final byte VERSION = 0x01;\n\n    /** Identity */\n    public static final int NODATA_RESPONSE_LENGTH     = 106;  // bytes\n    public static final int MESSAGE_RESPONSE_LENGTH    = 310;  // bytes\n    public static final int IDENTITY_RESPONSE_LENGTH   = 141;  // bytes\n    public static final int MESSAGE_BODY_LENGTH        = 140;  // bytes\n    public static final int ALIAS_LENGTH               = 35;   // bytes\n\n    private static final ByteBuffer sTimeStampBuffer = ByteBuffer.allocate(Long.SIZE / 8);\n\n    static {\n        sTimeStampBuffer.order(ByteOrder.LITTLE_ENDIAN);\n    }\n\n    /** Outgoing\n     *\n     * Create raw transmission data from protocol Objects\n    */\n\n    // TODO : Make this API consistent. Either all byte[] or all Packet structures\n    // The below method would be unnecessary if we ensured the rawPacket attribute\n    // was created on OwnedIdentityPacket's construction. We should only ever have\n    // to serialize our own identity. Every other identity is received serialized.\n    @Nullable\n    public byte[] serializeIdentity(@NonNull OwnedIdentityPacket ownedIdentity) {\n        // Protocol version 1\n        //[[version=1][timestamp=8][sender_public_key=32][display_name=35]][signature=64]\n        try {\n            byte[] identity = new byte[IDENTITY_RESPONSE_LENGTH];\n            int writeIndex = 0;\n            writeIndex += addVersionToBuffer(identity, writeIndex);\n            writeIndex += addTypeToBuffer(identity, IdentityPacket.TYPE, writeIndex);\n            writeIndex += addTimestampToBuffer(identity, writeIndex);\n            writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, identity, writeIndex);\n            writeIndex += addAliasToBuffer(ownedIdentity.alias, identity, writeIndex);\n            writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, identity, writeIndex);\n\n            if (writeIndex != IDENTITY_RESPONSE_LENGTH)\n                throw new IllegalStateException(\"Generated Identity does not match expected length\");\n\n            return identity;\n        } catch (UnsupportedEncodingException e) {\n            Log.e(TAG, \"Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?\");\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    @Nullable\n    public MessagePacket serializeMessage(@NonNull OwnedIdentityPacket ownedIdentity, String body) {\n        // Protocol version 1\n        //[[version=1][timestamp=8][sender_public_key=32][message=140][reply_signature=64]][signature=64]\n        try {\n            byte[] message = new byte[MESSAGE_RESPONSE_LENGTH];\n            int writeIndex = 0;\n            writeIndex += addVersionToBuffer(message, writeIndex);\n            writeIndex += addTypeToBuffer(message, MessagePacket.TYPE, writeIndex);\n            writeIndex += addTimestampToBuffer(message, writeIndex);\n            writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, message, writeIndex);\n            writeIndex += addMessageBodyToBuffer(body, message, writeIndex);\n            writeIndex += 64; // Empty reply_signature\n            writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, message, writeIndex);\n\n            if (writeIndex != MESSAGE_RESPONSE_LENGTH)\n                throw new IllegalStateException(\"Generated Message does not match expected length\");\n\n            return deserializeMessageWithIdentity(message, ownedIdentity);\n        } catch (UnsupportedEncodingException e) {\n            Log.e(TAG, \"Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?\");\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    @NonNull\n    public NoDataPacket serializeNoDataPacket(@NonNull OwnedIdentityPacket ownedIdentity) {\n        byte[] noDataPkt = new byte[NODATA_RESPONSE_LENGTH];\n        int writeIndex = 0;\n        writeIndex += addVersionToBuffer(noDataPkt, writeIndex);\n        writeIndex += addTypeToBuffer(noDataPkt, NoDataPacket.TYPE, writeIndex);\n        writeIndex += addTimestampToBuffer(noDataPkt, writeIndex);\n        writeIndex += addPublicKeyToBuffer(ownedIdentity.publicKey, noDataPkt, writeIndex);\n        writeIndex += addSignatureToBuffer(ownedIdentity.secretKey, noDataPkt, writeIndex);\n\n        if (writeIndex != NODATA_RESPONSE_LENGTH)\n            throw new IllegalStateException(\"Generated Message does not match expected length\");\n\n        return deserializeNoDataPacket(noDataPkt);\n    }\n\n    /** Incoming\n     *\n     * Produce protocol Objects from raw transmission data\n     */\n\n    @Nullable\n    public IdentityPacket deserializeIdentity(@NonNull byte[] identity) {\n        if (identity.length != IDENTITY_RESPONSE_LENGTH)\n            throw new IllegalArgumentException(String.format(\"Identity response is %d bytes. Expect %d\", identity.length, IDENTITY_RESPONSE_LENGTH));\n\n        // Protocol version 1\n        //[[version=1][type=1][timestamp=8][sender_public_key=32][display_name=35]][signature=64]\n        try {\n            int readIndex     = 0;\n            byte[] timestamp  = new byte[Long.SIZE / 8];\n            byte[] public_key = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];\n            byte[] alias      = new byte[ALIAS_LENGTH];\n            byte[] signature  = new byte[SodiumShaker.crypto_sign_BYTES];\n            byte[] signedData = new byte[IDENTITY_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];\n\n            readIndex += assertBufferVersion(identity, readIndex);\n            readIndex += assertBufferType(identity, IdentityPacket.TYPE, readIndex);\n            readIndex += getBytesFromBuffer(identity, timestamp, readIndex);\n            readIndex += getBytesFromBuffer(identity, public_key, readIndex);\n            readIndex += getBytesFromBuffer(identity, alias, readIndex);\n            readIndex += getBytesFromBuffer(identity, signature, readIndex);\n\n            System.arraycopy(identity, 0, signedData, 0, signedData.length);\n            boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);\n            if (!validSignature)\n                throw new IllegalStateException(\"Identity signature does not match content!\");\n\n            return new IdentityPacket(public_key, new String(alias, \"UTF-8\"), getDateFromTimestampBuffer(timestamp), identity);\n        } catch (UnsupportedEncodingException e) {\n            Log.e(TAG, \"Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?\");\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    @Nullable\n    public MessagePacket deserializeMessageWithIdentity(@NonNull byte[] message, @NonNull IdentityPacket identity) {\n        if (message.length != MESSAGE_RESPONSE_LENGTH)\n            throw new IllegalArgumentException(String.format(\"Message response is illegal length. Got %d expected %d\", message.length, MESSAGE_RESPONSE_LENGTH));\n\n        // TODO Don't duplicate this code between de\n        MessagePacket messageWithoutIdentity = deserializeMessage(message);\n        MessagePacket messageWithIdentity = null;\n\n        if (messageWithoutIdentity != null) {\n            messageWithIdentity = MessagePacket.attachIdentityToMessage(messageWithoutIdentity, identity);\n        }\n        return messageWithIdentity;\n    }\n\n    @Nullable\n    public MessagePacket deserializeMessage(@NonNull byte[] message) {\n        if (message.length != MESSAGE_RESPONSE_LENGTH)\n            throw new IllegalArgumentException(String.format(\"Message response is illegal length. Got %d expected %d\", message.length, MESSAGE_RESPONSE_LENGTH));\n\n        // Protocol version 1\n        //[[version=1][type=1][timestamp=8][sender_public_key=32][message=140][reply_signature=64]][signature=64]\n        try {\n            int readIndex          = 0;\n            byte[] timestamp       = new byte[Long.SIZE / 8];\n            byte[] public_key      = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];\n            byte[] body            = new byte[MESSAGE_BODY_LENGTH];\n            byte[] signature       = new byte[SodiumShaker.crypto_sign_BYTES];\n            byte[] replySignature  = new byte[SodiumShaker.crypto_sign_BYTES];\n            byte[] signedData      = new byte[MESSAGE_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];\n\n            readIndex += assertBufferVersion(message, readIndex);\n            readIndex += assertBufferType(message, MessagePacket.TYPE, readIndex);\n            readIndex += getBytesFromBuffer(message, timestamp, readIndex);\n            readIndex += getBytesFromBuffer(message, public_key, readIndex);\n            readIndex += getBytesFromBuffer(message, body, readIndex);\n            readIndex += getBytesFromBuffer(message, replySignature, readIndex);\n            readIndex += getBytesFromBuffer(message, signature, readIndex);\n\n            System.arraycopy(message, 0, signedData, 0, signedData.length);\n            boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);\n            if (!validSignature)\n                throw new IllegalStateException(\"Message signature does not match content!\");\n\n            return new MessagePacket(public_key, signature, replySignature, getDateFromTimestampBuffer(timestamp), new String(body, \"UTF-8\"), message);\n        } catch (UnsupportedEncodingException e) {\n            Log.e(TAG, \"Failed to generate Identity response. Are there invalid UTF-8 characters in the user alias?\");\n            e.printStackTrace();\n        }\n        return null;\n    }\n\n    @NonNull\n    public NoDataPacket deserializeNoDataPacket(@NonNull byte[] noDataPkt) {\n        if (noDataPkt.length != NODATA_RESPONSE_LENGTH)\n            throw new IllegalArgumentException(String.format(\"NoData response is %d bytes. Expect %d\", noDataPkt.length, IDENTITY_RESPONSE_LENGTH));\n\n        // Protocol version 1\n        // [[version=1][type=1][timestamp=8][sender_public_key=32]][signature=64]\n        int readIndex     = 0;\n        byte[] timestamp  = new byte[Long.SIZE / 8];\n        byte[] public_key = new byte[SodiumShaker.crypto_sign_PUBLICKEYBYTES];\n        byte[] signature  = new byte[SodiumShaker.crypto_sign_BYTES];\n        byte[] signedData = new byte[NODATA_RESPONSE_LENGTH - SodiumShaker.crypto_sign_BYTES];\n\n        readIndex += assertBufferVersion(noDataPkt, readIndex);\n        readIndex += assertBufferType(noDataPkt, NoDataPacket.TYPE, readIndex);\n        readIndex += getBytesFromBuffer(noDataPkt, timestamp, readIndex);\n        readIndex += getBytesFromBuffer(noDataPkt, public_key, readIndex);\n        readIndex += getBytesFromBuffer(noDataPkt, signature, readIndex);\n\n        System.arraycopy(noDataPkt, 0, signedData, 0, signedData.length);\n        boolean validSignature = SodiumShaker.verifySignature(public_key, signature, signedData);\n        if (!validSignature)\n            throw new IllegalStateException(\"NoData signature does not match content!\");\n\n        return new NoDataPacket(public_key, getDateFromTimestampBuffer(timestamp), signature, noDataPkt);\n    }\n\n\n    public byte getPacketType(@NonNull byte[] message) {\n        byte[] type = new byte[1];\n        getTypeFromBuffer(message, type, 1);\n        return type[0];\n    }\n\n    // </editor-fold desc=\"Public API\">\n\n    // <editor-fold desc=\"Private API\">\n\n    private static int addVersionToBuffer(@NonNull byte[] input, int offset) {\n        int bytesToWrite = 1;\n        assertBufferLength(input, offset + bytesToWrite);\n\n        input[offset] = VERSION;\n        return bytesToWrite;\n    }\n\n    private static int getVersionFromBuffer(@NonNull byte[] input, @NonNull byte[] version, int offset) {\n        int bytesToRead = 1;\n        assertBufferLength(input, offset + bytesToRead);\n        version[0] = input[offset];\n        return bytesToRead;\n    }\n\n    private static int addTypeToBuffer(@NonNull byte[] input, byte type, int offset) {\n        int bytesToWrite = 1;\n        assertBufferLength(input, offset + bytesToWrite);\n\n        input[offset] = type;\n        return bytesToWrite;\n    }\n\n    private static int getTypeFromBuffer(@NonNull byte[] input, @NonNull byte[] type, int offset) {\n        int bytesToRead = 1;\n        assertBufferLength(input, offset + bytesToRead);\n        type[0] = input[offset];\n        return bytesToRead;\n    }\n\n    private static int addTimestampToBuffer(@NonNull byte[] input, int offset) {\n        synchronized (sTimeStampBuffer) {\n            int bytesToWrite = Long.SIZE / 8;\n            assertBufferLength(input, offset + bytesToWrite);\n\n            long unixTime64 = System.currentTimeMillis();\n            sTimeStampBuffer.rewind();\n            sTimeStampBuffer.putLong(unixTime64);\n            System.arraycopy(sTimeStampBuffer.array(), 0, input, offset, bytesToWrite);\n            return bytesToWrite;\n        }\n    }\n\n    private static int addPublicKeyToBuffer(@NonNull byte[] public_key, @NonNull byte[] input, int offset) {\n        int bytesToWrite = public_key.length;\n        assertBufferLength(input, offset + bytesToWrite);\n\n        System.arraycopy(public_key, 0, input, offset, bytesToWrite);\n        return bytesToWrite;\n    }\n\n    private static int addAliasToBuffer(@NonNull String alias, @NonNull byte[] input, int offset) throws UnsupportedEncodingException {\n        int bytesToWrite = ALIAS_LENGTH;\n        assertBufferLength(input, offset + bytesToWrite);\n\n        byte[] aliasAsBytes = alias.getBytes(\"UTF-8\");\n        byte[] paddedAliasAsBytes = new byte[ALIAS_LENGTH];\n\n        truncateOrPadTextBuffer(aliasAsBytes, paddedAliasAsBytes);\n\n        System.arraycopy(paddedAliasAsBytes, 0, input, offset, bytesToWrite);\n        return bytesToWrite;\n    }\n\n    private static int addMessageBodyToBuffer(@NonNull String body, @NonNull byte[] input, int offset) throws UnsupportedEncodingException {\n        int bytesToWrite = MESSAGE_BODY_LENGTH;\n        assertBufferLength(input, offset + bytesToWrite);\n\n        byte[] bodyAsBytes = body.getBytes(\"UTF-8\");\n        byte[] paddedBodyAsBytes = new byte[MESSAGE_BODY_LENGTH];\n\n        truncateOrPadTextBuffer(bodyAsBytes, paddedBodyAsBytes);\n\n        System.arraycopy(paddedBodyAsBytes, 0, input, offset, bytesToWrite);\n        return bytesToWrite;\n    }\n\n    private static int getBytesFromBuffer(@NonNull byte[] input, @NonNull byte[] output, int offset) {\n        int bytesToRead = output.length;\n        assertBufferLength(input, offset + bytesToRead);\n\n        System.arraycopy(input, offset, output, 0, bytesToRead);\n        return bytesToRead;\n    }\n\n    /**\n     * Generate signature for input from the first byte until the offset byte. Append signature to input after offset byte.\n     */\n    private static int addSignatureToBuffer(@NonNull byte[] secret_key, @NonNull byte[] input, int offset) {\n        int bytesToWrite = SodiumShaker.crypto_sign_BYTES;\n        assertBufferLength(input, offset + bytesToWrite);\n\n        byte[] signature = SodiumShaker.generateSignatureForMessage(secret_key, input, offset);\n\n        System.arraycopy(signature, 0, input, offset, bytesToWrite);\n        return bytesToWrite;\n    }\n\n    /** Utility */\n\n    /**\n     * Truncates or pads input to fit precisely inside output.\n     * After this call output will contain the truncated or padded input\n     */\n    private static void truncateOrPadTextBuffer(byte[] input, byte[] output) {\n        System.arraycopy(input, 0, output, 0, Math.min(input.length, output.length));\n        if (input.length < output.length) {\n            for (int x = input.length; x < output.length; x++) {\n                output[x] = 0x20; // UTF-8 space\n            }\n        }\n    }\n\n    private static void assertBufferLength(byte[] input, int minimumLength) {\n        if (input.length < minimumLength)\n            throw new IllegalArgumentException(String.format(\"Operation requires input buffer length %d. Actual: %d\", minimumLength, input.length));\n    }\n\n    private static int assertBufferVersion(byte[] input, int offset) {\n        byte[] version = new byte[1];\n        getVersionFromBuffer(input, version, offset);\n\n        if (version[0] != VERSION)\n            throw new IllegalStateException(String.format(\"Response is for an unknown protocol version. Got %d. Expected %d\", version[0], VERSION));\n        return 1;\n    }\n\n    private static int assertBufferType(byte[] input, byte expectedType, int offset) {\n        byte[] type = new byte[1];\n        getTypeFromBuffer(input, type, offset);\n\n        if (type[0] != expectedType)\n            throw new IllegalStateException(String.format(\"Response is for an unexpected message type. Got %d. Expected %d\", type[0], expectedType));\n        return 1;\n    }\n\n    @Nullable\n    private static Date getDateFromTimestampBuffer(byte[] timestamp) {\n        synchronized (sTimeStampBuffer) {\n            sTimeStampBuffer.clear();\n            sTimeStampBuffer.put(timestamp);\n            sTimeStampBuffer.rewind();\n            // TODO: Test if flip needed\n            return new Date(sTimeStampBuffer.getLong());\n        }\n    }\n\n    // </editor-fold desc=\"Private API\">\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/IdentityPacket.java",
    "content": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\nimport java.util.Date;\n\n/**\n * An identity for a remote peer\n * Created by davidbrodsky on 10/13/14.\n */\npublic class IdentityPacket {\n    public static final byte TYPE = 0x01;\n\n    public final byte[] publicKey;\n    public final Date   dateSeen;\n    public final String alias;\n    public final byte[] rawPacket;\n\n    public IdentityPacket(@NonNull final byte[] publicKey, @Nullable String alias, @NonNull Date dateSeen,\n                          @NonNull final byte[] rawPacket) {\n        // dateSeen is allowed null because it's meaningless for OwnedIdentities\n        this.publicKey  = publicKey;\n        this.alias      = alias == null ? null : alias.trim();\n        this.dateSeen   = dateSeen;\n        this.rawPacket  = rawPacket;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/MessagePacket.java",
    "content": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\n\nimport java.util.Date;\n\n/**\n * Created by davidbrodsky on 10/15/14.\n */\npublic class MessagePacket {\n    public static final byte TYPE = 0x02;\n\n    final public IdentityPacket sender;\n    final public String body;\n    final public Date authoredDate;\n    final public byte[] signature;\n    final public byte[] replySig;\n    final public byte[] rawPacket;\n\n    /** Incoming */\n    public MessagePacket(@NonNull final byte[] publicKey,\n                         @NonNull byte[] signature,\n                         @NonNull byte[] replySig,\n                         @NonNull Date authoredDate,\n                         @NonNull String body,\n                         @NonNull byte[] rawPacket) {\n\n        this.body         = body;\n        this.signature    = signature;\n        this.replySig     = replySig;\n        this.rawPacket    = rawPacket;\n        this.authoredDate = authoredDate;\n        sender            = new IdentityPacket(publicKey, null, null, null); // We don't have the sender's full identity response\n    }\n\n    public static MessagePacket attachIdentityToMessage(@NonNull MessagePacket message, @NonNull IdentityPacket identity) {\n        return new MessagePacket(identity, message.signature, message.replySig, message.body, message.rawPacket, message.authoredDate);\n    }\n\n    /** Outgoing */\n    public MessagePacket(@NonNull IdentityPacket sender,\n                         @NonNull byte[] signature,\n                         @NonNull byte[] replySig,\n                         @NonNull String body,\n                         @NonNull byte[] rawPacket,\n                         @NonNull Date authoredDate) {\n\n        this.body         = body.trim();\n        this.signature    = signature;\n        this.replySig     = replySig;\n        this.rawPacket    = rawPacket;\n        this.authoredDate = authoredDate;\n        this.sender       = sender;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/NoDataPacket.java",
    "content": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\n\nimport java.util.Date;\n\n/**\n * Created by davidbrodsky on 10/15/14.\n *\n */\npublic class NoDataPacket {\n    public static final byte TYPE = 0x03;\n\n    final public byte[] publicKey;\n    final public Date authoredDate;\n    final public byte[] signature;\n    final public byte[] rawPacket;\n\n    public NoDataPacket(@NonNull final byte[] publicKey,\n                        @NonNull Date authoredDate,\n                        @NonNull byte[] signature,\n                        @NonNull byte[] rawPacket) {\n\n        this.publicKey    = publicKey;\n        this.signature    = signature;\n        this.rawPacket    = rawPacket;\n        this.authoredDate = authoredDate;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/OwnedIdentityPacket.java",
    "content": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\n\n/**\n * An Identity for the local peer\n * Created by davidbrodsky on 10/13/14.\n */\npublic class OwnedIdentityPacket extends IdentityPacket {\n\n    public final byte[] secretKey;\n\n    public OwnedIdentityPacket(@NonNull final byte[] secretKey, @NonNull final byte[] publicKey,\n                               @NonNull String alias, byte[] rawPacket) {\n        super(publicKey, alias, null, rawPacket);\n        this.secretKey = secretKey;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/protocol/Protocol.java",
    "content": "package pro.dbro.ble.protocol;\n\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\n\n/**\n * Created by davidbrodsky on 10/20/14.\n */\npublic interface Protocol {\n\n    /** Outgoing\n     *\n     * Serialize Protocol Objects to raw transmission data\n    **/\n\n    // TODO Decide on a consistent API here\n    public byte[] serializeIdentity(@NonNull OwnedIdentityPacket ownedIdentity);\n\n    public MessagePacket serializeMessage(@NonNull OwnedIdentityPacket ownedIdentity, String body);\n\n    public NoDataPacket serializeNoDataPacket(@NonNull OwnedIdentityPacket ownedIdentity);\n\n    /** Incoming\n     *\n     * Deserialize raw transmission data into Protocol Objects\n     */\n\n    public IdentityPacket deserializeIdentity(@NonNull byte[] identity);\n\n    /** Deserialize a message where the author identity is known */\n    public MessagePacket deserializeMessageWithIdentity(@NonNull byte[] message, @Nullable IdentityPacket identity);\n\n    /** Deserialize a message where the author identity is not known */\n    public MessagePacket deserializeMessage(@NonNull byte[] message);\n\n    public byte getPacketType(@NonNull byte[] message);\n\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/Notification.java",
    "content": "package pro.dbro.ble.ui;\n\nimport android.app.NotificationManager;\nimport android.app.PendingIntent;\nimport android.app.TaskStackBuilder;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.graphics.Bitmap;\nimport android.graphics.Canvas;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.support.v4.app.NotificationCompat;\nimport android.view.View;\n\nimport java.util.ArrayList;\n\nimport im.delight.android.identicons.SymmetricIdenticon;\nimport pro.dbro.ble.R;\nimport pro.dbro.ble.data.model.DataUtil;\nimport pro.dbro.ble.data.model.Message;\nimport pro.dbro.ble.data.model.Peer;\nimport pro.dbro.ble.ui.activities.MainActivity;\n\n/**\n * Created by davidbrodsky on 11/14/14.\n */\npublic class Notification {\n\n    /** Notification Ids */\n    private static final int MESSAGE_NOTIFICATION_ID = 1;\n    private static final int PEER_AVAILABLE_NOTIFICATION_ID = 2;\n\n    private static final int MAX_MESSAGES_TO_SHOW = 6;\n\n    private static final ArrayList<String> sNotificationInboxItems = new ArrayList<>(MAX_MESSAGES_TO_SHOW + 1);\n\n    // <editor-fold desc=\"Public API\">\n\n    /**\n     * Display a notification representing peer being available, or remove any indicating such\n     * if isAvailable is false.\n     *\n     * Does not call peer.close()\n     */\n    public static void displayPeerAvailableNotification(@NonNull Context context, @NonNull Peer peer, boolean isAvailable) {\n        NotificationManager mNotificationManager =\n                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);\n\n        if (!isAvailable) {\n            mNotificationManager.cancel(DataUtil.bytesToHex(peer.getPublicKey()), PEER_AVAILABLE_NOTIFICATION_ID);\n            return;\n        }\n        if (peer.getAlias() == null) return; // TODO : Notify of peers without alias?\n\n        String title = String.format(\"%s is nearby\", peer.getAlias());\n\n        Intent resultIntent = new Intent(context, MainActivity.class);\n\n        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);\n        builder.setSmallIcon(R.mipmap.ic_launcher);\n        builder.setContentTitle(title);\n        builder.setContentIntent(makePendingIntent(context, resultIntent));\n        builder.setContentText(context.getString(R.string.notification_touch_to_chat));\n\n        mNotificationManager.notify(DataUtil.bytesToHex(peer.getPublicKey()), PEER_AVAILABLE_NOTIFICATION_ID, builder.build());\n    }\n\n    /**\n     * Display a notification representing a new received message. Multiple calls to this method are displayed as a single\n     * notification, showing a preview of the last MAX_MESSAGES_TO_SHOW messages.\n     *\n     * Does not call message.close() or sender.close()\n     */\n    public static void displayMessageNotification(@NonNull Context context, @NonNull Message message, @Nullable Peer sender) {\n        StringBuilder nBuilder = new StringBuilder();\n        if (sender != null && sender.getAlias() != null) {\n            nBuilder.append(sender.getAlias());\n            nBuilder.append(\": \");\n        }\n        nBuilder.append(message.getBody().length() > 80 ?\n                            message.getBody().substring(0, 80) + \"...\" :\n                            message.getBody());\n        sNotificationInboxItems.add(nBuilder.toString());\n        if (sNotificationInboxItems.size() > MAX_MESSAGES_TO_SHOW) sNotificationInboxItems.remove(sNotificationInboxItems.size()-1);\n\n        Intent resultIntent = new Intent(context, MainActivity.class);\n\n        NotificationCompat.InboxStyle inboxStyle =\n                new NotificationCompat.InboxStyle();\n        inboxStyle.setBigContentTitle(context.getString(R.string.notification_new_messages));\n\n        for (String inboxItem : sNotificationInboxItems) {\n            inboxStyle.addLine(inboxItem);\n        }\n\n        SymmetricIdenticon identicon = new SymmetricIdenticon(context);\n        identicon.show(new String(sender.getPublicKey()));\n\n        NotificationCompat.Builder builder = new NotificationCompat.Builder(context);\n        builder.setContentTitle(context.getString(R.string.notification_new_messages));\n        builder.setLargeIcon(loadBitmapFromView(identicon, 640, 480));\n        builder.setSmallIcon(R.mipmap.ic_launcher);\n        builder.setContentIntent(makePendingIntent(context, resultIntent));\n        builder.setStyle(inboxStyle);\n        builder.setContentText(sNotificationInboxItems.get(0));\n        builder.setAutoCancel(true);\n        builder.setCategory(NotificationCompat.CATEGORY_MESSAGE);\n        builder.setVibrate(new long[] { 500, 500, 500, 500});\n\n        NotificationManager mNotificationManager =\n                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);\n\n        mNotificationManager.notify(MESSAGE_NOTIFICATION_ID, builder.build());\n    }\n\n    // </editor-fold desc=\"Public API\">\n\n    // <editor-fold desc=\"Private API\">\n\n    private static PendingIntent makePendingIntent(@NonNull Context context, @NonNull Intent resultIntent) {\n        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);\n        // Adds the back stack\n        stackBuilder.addParentStack(MainActivity.class);\n        // Adds the Intent to the top of the stack\n        stackBuilder.addNextIntent(resultIntent);\n        // Gets a PendingIntent containing the entire back stack\n        return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);\n    }\n\n    public static Bitmap loadBitmapFromView(View v, int width, int height) {\n\n        int measuredWidth = View.MeasureSpec.makeMeasureSpec(width,\n                View.MeasureSpec.EXACTLY);\n        int measuredHeight = View.MeasureSpec.makeMeasureSpec(height,\n                View.MeasureSpec.EXACTLY);\n        v.measure(measuredWidth, measuredHeight);\n        v.layout(0, 0, v.getMeasuredWidth(),v.getMeasuredHeight());\n\n        Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);\n        Canvas c = new Canvas(b);\n        v.draw(c);\n        return b;\n    }\n\n    // </editor-fold desc=\"Private API\">\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/activities/LogConsumer.java",
    "content": "package pro.dbro.ble.ui.activities;\n\n/**\n * Created by davidbrodsky on 10/11/14.\n */\npublic interface LogConsumer {\n    public void onLogEvent(String event);\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/activities/MainActivity.java",
    "content": "package pro.dbro.ble.ui.activities;\n\nimport android.animation.ValueAnimator;\nimport android.app.Activity;\nimport android.app.AlertDialog;\nimport android.app.FragmentManager;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.graphics.Bitmap;\nimport android.graphics.drawable.ColorDrawable;\nimport android.os.Bundle;\nimport android.support.annotation.NonNull;\nimport android.support.v4.app.Fragment;\nimport android.support.v4.app.FragmentTransaction;\nimport android.support.v4.widget.DrawerLayout;\nimport android.support.v7.app.ActionBarDrawerToggle;\nimport android.support.v7.app.AppCompatActivity;\nimport android.support.v7.graphics.Palette;\nimport android.support.v7.widget.Toolbar;\nimport android.transition.Slide;\nimport android.transition.TransitionSet;\nimport android.util.Log;\nimport android.view.Gravity;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.animation.AccelerateDecelerateInterpolator;\nimport android.widget.AdapterView;\nimport android.widget.EditText;\nimport android.widget.Spinner;\nimport android.widget.TextView;\n\nimport com.nispok.snackbar.Snackbar;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\n\nimport butterknife.ButterKnife;\nimport butterknife.InjectView;\nimport hugo.weaving.DebugLog;\nimport im.delight.android.identicons.SymmetricIdenticon;\nimport pro.dbro.airshare.app.AirShareService;\nimport pro.dbro.airshare.app.ui.AirShareFragment;\nimport pro.dbro.ble.ChatClient;\nimport pro.dbro.ble.ChatPeerFlow;\nimport pro.dbro.ble.PrefsManager;\nimport pro.dbro.ble.R;\nimport pro.dbro.ble.data.model.Peer;\nimport pro.dbro.ble.protocol.OwnedIdentityPacket;\nimport pro.dbro.ble.ui.Notification;\nimport pro.dbro.ble.ui.adapter.StatusArrayAdapter;\nimport pro.dbro.ble.ui.fragment.MessagingFragment;\nimport pro.dbro.ble.ui.fragment.ProfileFragment;\nimport pro.dbro.ble.ui.fragment.WelcomeFragment;\nimport timber.log.Timber;\n\npublic class MainActivity extends AppCompatActivity implements LogConsumer,\n        WelcomeFragment.WelcomeFragmentCallback,\n        AirShareFragment.Callback,\n        MessagingFragment.ChatFragmentCallback, ChatClient.Callback {\n\n    public static final String TAG = \"MainActivity\";\n\n    private ActionBarDrawerToggle mDrawerToggle;\n    private MessagingFragment mMessagingFragment;\n    private OwnedIdentityPacket mUserIdentity;\n\n    private ChatClient mClient;\n    private AirShareFragment mAirShareFragment;\n\n    private Palette mPalette;\n\n//    private PeerAdapter mPeerAdapter;\n\n    @InjectView(R.id.status_spinner)\n    Spinner mStatusSpinner;\n\n    @InjectView(R.id.log)\n    TextView mLogView;\n\n//    @InjectView(R.id.peer_recyclerview)\n//    RecyclerView mPeerRecyclerView;\n\n    @InjectView(R.id.toolbar)\n    Toolbar mToolbar;\n\n    @InjectView(R.id.my_drawer_layout)\n    DrawerLayout mDrawer;\n\n    @InjectView(R.id.msg_pass_count)\n    TextView mMessagesPassedCount;\n\n    @InjectView(R.id.peers_met_count)\n    TextView mPeersMetCount;\n\n    @InjectView(R.id.profile_identicon)\n    SymmetricIdenticon mProfileIdenticon;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_main);\n        ButterKnife.inject(this);\n\n        mClient = new ChatClient(this);\n\n//        mLogView.setOnLongClickListener(new View.OnLongClickListener() {\n//            @Override\n//            public boolean onLongClick(View view) {\n//                mLogView.setText(\"\");\n//                return false;\n//            }\n//        });\n\n        mStatusSpinner.setAdapter(new StatusArrayAdapter(this, new ArrayList<>(Arrays.asList(getResources().getStringArray(R.array.status_options)))));\n        mStatusSpinner.setEnabled(false);\n        mStatusSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {\n            @Override\n            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n                switch (position) {\n\n                    case 0: // Always online\n                        mClient.makeAvailable();\n                        mAirShareFragment.setShouldServiceContinueInBackground(true);\n                        break;\n\n                    case 1: // Online when using app\n                        mClient.makeAvailable();\n                        mAirShareFragment.setShouldServiceContinueInBackground(false);\n                        break;\n\n                    case 2: // Offline\n                        mClient.makeUnavailable();\n//                        mPeerAdapter.clearPeers();\n                        break;\n                }\n                PrefsManager.setStatus(MainActivity.this, position);\n            }\n\n            @Override\n            public void onNothingSelected(AdapterView<?> parent) {\n                // do nothing\n            }\n        });\n\n        setSupportActionBar(mToolbar);\n        setTitle(getString(R.string.public_feed));\n        mToolbar.setTitleTextColor(getResources().getColor(android.R.color.white));\n//        mToolbar.setNavigationIcon(R.drawable.ic_drawer);\n        getSupportActionBar().setDisplayHomeAsUpEnabled(true);\n        getSupportActionBar().setHomeButtonEnabled(true);\n\n        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawer,\n                mToolbar, R.string.drawer_open, R.string.drawer_close) {\n\n            /** Called when a drawer has settled in a completely closed state. */\n            public void onDrawerClosed(View view) {\n                super.onDrawerClosed(view);\n                invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()\n            }\n\n            /** Called when a drawer has settled in a completely open state. */\n            public void onDrawerOpened(View drawerView) {\n                super.onDrawerOpened(drawerView);\n                refreshProfileStats();\n                invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()\n            }\n        };\n\n        // Override ActionB\n        mToolbar.setNavigationOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                if (getSupportFragmentManager().getBackStackEntryCount() == 0)\n                    mDrawer.openDrawer(Gravity.START);\n                else\n                    getSupportFragmentManager().popBackStack();\n            }\n        });\n\n        mDrawer.setDrawerListener(mDrawerToggle);\n        mDrawerToggle.syncState();\n\n        checkUserRegistered();\n\n//        mPeerAdapter = new PeerAdapter(this, new ArrayList<Peer>());\n//        mPeerRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));\n//        mPeerRecyclerView.setAdapter(mPeerAdapter);\n\n        getSupportFragmentManager().addOnBackStackChangedListener(new android.support.v4.app.FragmentManager.OnBackStackChangedListener() {\n            @Override\n            public void onBackStackChanged() {\n                int numEntries = getSupportFragmentManager().getBackStackEntryCount();\n                if (numEntries == 0) {\n                    mMessagingFragment.animateIn();\n                    tintSystemBars(mPalette.getVibrantColor(R.color.primary), mPalette.getDarkVibrantColor(R.color.primaryDark),\n                            getResources().getColor(R.color.primary), getResources().getColor(R.color.primaryDark));\n\n                    // Hack animate the drawer icon\n                    ValueAnimator drawerAnimator = ValueAnimator.ofFloat(1f, 0f);\n                    drawerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n                        @Override\n                        public void onAnimationUpdate(ValueAnimator animation) {\n                            mDrawerToggle.onDrawerSlide(null, (Float) animation.getAnimatedValue());\n                        }\n                    });\n                    drawerAnimator.start();\n                    setTitle(getString(R.string.public_feed));\n                }\n            }\n        });\n    }\n\n    /**\n     * Adds the message list fragment and populates\n     * the profile navigation drawer with the user profile\n     */\n    private void revealChatViews() {\n        mMessagingFragment = new MessagingFragment();\n        mMessagingFragment.setDataStore(mClient.getDataStore());\n        getSupportFragmentManager().beginTransaction()\n                .replace(R.id.container, mMessagingFragment, \"messaging\")\n                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)\n                .commit();\n\n        mProfileIdenticon.show(new String(mUserIdentity.publicKey));\n        ((TextView) findViewById(R.id.profile_name)).setText(mUserIdentity.alias);\n    }\n\n    private void refreshProfileStats() {\n        mPeersMetCount.setText(String.valueOf(Math.max(0, mClient.getDataStore().countPeers() - 1))); //ignore self\n        mMessagesPassedCount.setText(String.valueOf(mClient.getDataStore().countMessagesPassed()));\n    }\n\n    /**\n     * LogConsumer interface\n     */\n\n    @Override\n    public void onLogEvent(final String event) {\n        /*\n        mLogView.post(new Runnable() {\n            @Override\n            public void run() {\n                mLogView.append(event + \"\\n\");\n\n            }\n        });\n        */\n    }\n\n    /**\n     * Check if a username has been registered and take appropriate action.\n     *\n     * If a username has not yet been selected, show WelcomeFragment\n     * If a username has been selected, initialize AirShare\n     */\n    private void checkUserRegistered() {\n        Peer localPeer = mClient.getPrimaryLocalPeer();\n        if (localPeer != null) {\n\n            // Register ourselves with the AirShare Service, using our own user model's alias\n            if (mAirShareFragment == null) {\n                mAirShareFragment = AirShareFragment.newInstance(localPeer.getAlias(), ChatClient.AIRSHARE_SERVICE_NAME, this);\n                Timber.d(\"Adding airshare frag\");\n                getSupportFragmentManager().beginTransaction()\n                        .add(mAirShareFragment, \"airshare\")\n                        .commit();\n            }\n\n        } else {\n\n            // Show WelcomeFragment to collect the user's desired username\n            // will be notified of result via #onNameChosen\n            mToolbar.setVisibility(View.GONE);\n            getWindow().setStatusBarColor(getResources().getColor(R.color.welcome_status_bar));\n            getSupportFragmentManager().beginTransaction()\n                    .replace(R.id.container, new WelcomeFragment())\n                    .commit();\n        }\n    }\n\n    @Override\n    public void onServiceReady(AirShareService.ServiceBinder serviceBinder) {\n        mUserIdentity = (OwnedIdentityPacket) mClient.getPrimaryLocalPeer().getIdentity();\n\n        mClient.setAirShareServiceBinder(serviceBinder);\n        mClient.setCallback(this);\n        mClient.makeAvailable();\n        mStatusSpinner.setEnabled(true);\n        mStatusSpinner.setSelection(PrefsManager.getStatus(this));\n        revealChatViews();\n        refreshProfileStats();\n    }\n\n    @Override\n    public void onFinished(Exception exception) {\n\n    }\n\n    @Override\n    public void onMessageSendRequested(String message) {\n        mClient.sendPublicMessageFromPrimaryIdentity(message);\n    }\n\n    @Override\n    public void onMessageSelected(View identictionView, View usernameView, int messageId, int peerId) {\n        // Create new fragment to add (Fragment B)\n        Peer peer = mClient.getDataStore().getPeerById(peerId);\n        if (peer == null) {\n            Log.w(TAG, \"Could not lookup peer. Cannot show profile\");\n            return;\n        }\n\n        setTitle(peer.getAlias());\n\n//        identictionView.setTransitionName(getString(R.string.identicon_transition_name));\n//        usernameView.setTransitionName(getString(R.string.username_transition_name));\n\n        Fragment profileFragment = ProfileFragment.createForPeer(mClient.getDataStore(), peer);\n\n//        final TransitionSet sharedElementTransition = new TransitionSet();\n//        sharedElementTransition.addTransition(new ChangeBounds());\n//        sharedElementTransition.addTransition(new ChangeTransform());\n//        sharedElementTransition.setInterpolator(new AccelerateDecelerateInterpolator());\n//        sharedElementTransition.setDuration(200);\n\n        final TransitionSet slideTransition = new TransitionSet();\n        slideTransition.addTransition(new Slide());\n        slideTransition.setInterpolator(new AccelerateDecelerateInterpolator());\n        slideTransition.setDuration(300);\n\n        profileFragment.setEnterTransition(slideTransition);\n        profileFragment.setReturnTransition(slideTransition);\n//        profileFragment.setSharedElementEnterTransition(sharedElementTransition);\n        profileFragment.setAllowEnterTransitionOverlap(false);\n        profileFragment.setAllowReturnTransitionOverlap(false);\n\n        // Message fragment performs an exit when Profile is added, and an enter when profile is popped\n//        getFragmentManager().findFragmentByTag(\"messaging\").setReenterTransition(slideTransition);\n//        getFragmentManager().findFragmentByTag(\"messaging\").setExitTransition(slideTransition);\n//        getFragmentManager().findFragmentByTag(\"messaging\").setSharedElementEnterTransition(sharedElementTransition);\n\n        getSupportFragmentManager().beginTransaction()\n                .replace(R.id.container, profileFragment)\n                .addToBackStack(\"profile\")\n//                .addSharedElement(identictionView, getString(R.string.identicon_transition_name))\n//                .addSharedElement(usernameView, getString(R.string.username_transition_name))\n                .commit();\n\n        Bitmap bitmap = Notification.loadBitmapFromView(identictionView, 100, 100);\n        Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() {\n            public void onGenerated(Palette p) {\n                mPalette = p;\n                tintSystemBars(getResources().getColor(R.color.primary), getResources().getColor(R.color.primaryDark),\n                        p.getVibrantColor(R.color.primary), p.getDarkVibrantColor(R.color.primaryDark));\n\n            }\n        });\n\n        // Hack animate the drawer icon\n        ValueAnimator drawerAnimator = ValueAnimator.ofFloat(0, 1f);\n        drawerAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                mDrawerToggle.onDrawerSlide(null, (Float) animation.getAnimatedValue());\n            }\n        });\n        drawerAnimator.start();\n    }\n\n    @Override\n    public void onAppPeerStatusUpdated(@NonNull Peer remotePeer, @NonNull ChatPeerFlow.Callback.ConnectionStatus status) {\n        Snackbar.with(getApplicationContext())\n                .position(Snackbar.SnackbarPosition.TOP)\n                .text(String.format(\"%s %s\",\n                        remotePeer.getAlias(),\n                        status == ChatPeerFlow.Callback.ConnectionStatus.CONNECTED ? \"connected\" : \"disconnected\"))\n                .show((ViewGroup) findViewById(R.id.container));\n\n//        switch (status) {\n//            case CONNECTED:\n//                mPeerAdapter.notifyPeerAdded(remotePeer);\n//                break;\n//\n//            case DISCONNECTED:\n//                mPeerAdapter.notifyPeerRemoved(remotePeer);\n//                break;\n//        }\n    }\n\n    private void tintSystemBars(final int toolbarFromColor, final int statusbarFromColor,\n                                final int toolbarToColor, final int statusbarToColor) {\n\n        ValueAnimator toolbarAnim = ValueAnimator.ofArgb(toolbarFromColor, toolbarToColor);\n        ValueAnimator statusbarAnim = ValueAnimator.ofArgb(statusbarFromColor, statusbarToColor);\n\n        statusbarAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                getWindow().setStatusBarColor((Integer) animation.getAnimatedValue());\n            }\n        });\n\n        toolbarAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {\n            @Override\n            public void onAnimationUpdate(ValueAnimator animation) {\n                getSupportActionBar().setBackgroundDrawable(new ColorDrawable((Integer) animation.getAnimatedValue()));\n            }\n        });\n\n        toolbarAnim.setDuration(500).start();\n        statusbarAnim.setDuration(500).start();\n    }\n\n    @Override\n    public void onBackPressed() {\n        if (getSupportFragmentManager().getBackStackEntryCount() > 0) {\n            getSupportFragmentManager().popBackStack();\n        } else {\n            super.onBackPressed();\n        }\n    }\n\n    @Override\n    public void onNameChosen(String name) {\n        mToolbar.setVisibility(View.VISIBLE);\n        getWindow().setStatusBarColor(getResources().getColor(R.color.primaryDark));\n        mClient.createPrimaryIdentity(name);\n        checkUserRegistered();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/activities/Util.java",
    "content": "package pro.dbro.ble.ui.activities;\n\nimport android.app.AlertDialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.support.annotation.NonNull;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.widget.EditText;\nimport android.widget.TextView;\n\nimport pro.dbro.ble.ChatClient;\nimport pro.dbro.ble.R;\n\n/**\n * Created by davidbrodsky on 10/13/14.\n */\npublic class Util {\n\n    public static void showWelcomeDialog(@NonNull final ChatClient app, @NonNull final Context context, DialogInterface.OnDismissListener dismissListener) {\n        View dialogView = ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))\n                            .inflate(R.layout.dialog_welcome, null);\n        final EditText aliasEntry = ((EditText) dialogView.findViewById(R.id.aliasEntry));\n\n        AlertDialog.Builder builder = new AlertDialog.Builder(context);\n        final AlertDialog dialog = builder.setTitle(context.getString(R.string.dialog_welcome_greeting))\n                .setView(dialogView)\n                .setPositiveButton(context.getString(R.string.dialog_ok), new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialogInterface, int i) {\n                        app.createPrimaryIdentity(aliasEntry.getText().toString());\n                    }\n                })\n                .setOnDismissListener(dismissListener)\n                .show();\n        aliasEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {\n            @Override\n            public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {\n                app.createPrimaryIdentity(textView.getText().toString());\n                dialog.dismiss();\n                return false;\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/CursorFilter.java",
    "content": "/*\n * Copyright (C) 2011 The Android Open Source Project\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 */\n\npackage pro.dbro.ble.ui.adapter;\n\nimport android.database.Cursor;\nimport android.widget.Filter;\n\n/**\n * The CursorFilter delegates most of the work to the\n * {@link android.widget.CursorAdapter}. Subclasses should override these\n * delegate methods to run the queries and convert the results into String\n * that can be used by auto-completion widgets.\n */\nclass CursorFilter extends Filter {\n\n    CursorFilterClient mClient;\n\n    interface CursorFilterClient {\n        CharSequence convertToString(Cursor cursor);\n        Cursor runQueryOnBackgroundThread(CharSequence constraint);\n        Cursor getCursor();\n        void changeCursor(Cursor cursor);\n    }\n\n    CursorFilter(CursorFilterClient client) {\n        mClient = client;\n    }\n\n    @Override\n    public CharSequence convertResultToString(Object resultValue) {\n        return mClient.convertToString((Cursor) resultValue);\n    }\n\n    @Override\n    protected FilterResults performFiltering(CharSequence constraint) {\n        Cursor cursor = mClient.runQueryOnBackgroundThread(constraint);\n\n        FilterResults results = new FilterResults();\n        if (cursor != null) {\n            results.count = cursor.getCount();\n            results.values = cursor;\n        } else {\n            results.count = 0;\n            results.values = null;\n        }\n        return results;\n    }\n\n    @Override\n    protected void publishResults(CharSequence constraint, FilterResults results) {\n        Cursor oldCursor = mClient.getCursor();\n\n        if (results.values != null && results.values != oldCursor) {\n            mClient.changeCursor((Cursor) results.values);\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/MessageAdapter.java",
    "content": "package pro.dbro.ble.ui.adapter;\n\nimport android.content.Context;\nimport android.database.Cursor;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.support.v7.widget.RecyclerView;\nimport android.text.format.DateUtils;\nimport android.util.Log;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport java.text.ParseException;\nimport java.util.UUID;\n\nimport im.delight.android.identicons.SymmetricIdenticon;\nimport pro.dbro.ble.R;\nimport pro.dbro.ble.data.DataStore;\nimport pro.dbro.ble.data.model.DataUtil;\nimport pro.dbro.ble.data.model.MessageTable;\nimport pro.dbro.ble.data.model.Peer;\n\n/**\n * Created by davidbrodsky on 10/19/14.\n */\npublic class MessageAdapter extends RecyclerViewCursorAdapter<MessageAdapter.ViewHolder> {\n    public static final String TAG = \"MessageAdapter\";\n\n    public interface MessageSelectedListener {\n        void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId);\n    }\n\n    private DataStore mDataStore;\n    private RecyclerView mHost;\n    private MessageSelectedListener mListener;\n\n    public static class ViewHolder extends RecyclerView.ViewHolder {\n        public View container;\n        public TextView senderView;\n        public TextView messageView;\n        public TextView authoredView;\n        public SymmetricIdenticon identicon;\n        public Peer peer;\n\n\n        public ViewHolder(View v) {\n            super(v);\n            container = v;\n            senderView = (TextView) v.findViewById(R.id.sender);\n            messageView = (TextView) v.findViewById(R.id.messageBody);\n            authoredView = (TextView) v.findViewById(R.id.authoredDate);\n            identicon = (SymmetricIdenticon) v.findViewById(R.id.identicon);\n\n        }\n    }\n\n    /**\n     * Recommended constructor.\n     *\n     * @param context       The context\n     * @param dataStore     The data backend\n     * @param fromPeer      A Peer to show messages from, or null to show all messages\n     * @param flags         Flags used to determine the behavior of the adapter;\n     *                Currently it accept {@link #FLAG_REGISTER_CONTENT_OBSERVER}.\n     */\n    public MessageAdapter(@NonNull Context context,\n                          @Nullable Peer fromPeer,\n                          @NonNull DataStore dataStore,\n                          @Nullable MessageSelectedListener listener,\n                          int flags) {\n        super(context,\n                fromPeer == null ? dataStore.getRecentMessages().getCursor() :\n                                   dataStore.getRecentMessagesByPeer(fromPeer).getCursor(), flags);\n        mDataStore = dataStore;\n        mListener = listener;\n    }\n\n    @Override\n    public void onAttachedToRecyclerView(RecyclerView recyclerView) {\n        mHost = recyclerView;\n    }\n\n    @Override\n    public void onBindViewHolder(ViewHolder holder, Cursor cursor) {\n        holder.container.setTag(R.id.view_tag_msg_id, cursor.getInt(cursor.getColumnIndex(MessageTable.id)));\n\n        if (holder.peer == null) // TODO : Should do this lookup on a background thread\n            holder.peer = mDataStore.getPeerById(cursor.getInt(cursor.getColumnIndex(MessageTable.peerId)));\n\n        if (holder.peer != null) {\n            holder.container.setTag(R.id.view_tag_peer_id, holder.peer.getId());\n            holder.senderView.setText(holder.peer.getAlias());\n            holder.identicon.show(new String(holder.peer.getPublicKey()));\n        } else {\n            holder.senderView.setText(\"?\");\n            holder.identicon.show(UUID.randomUUID());\n        }\n        holder.messageView.setText(cursor.getString(cursor.getColumnIndex(MessageTable.body)));\n        try {\n            holder.authoredView.setText(DateUtils.getRelativeTimeSpanString(\n                    DataUtil.storedDateFormatter.parse(cursor.getString(cursor.getColumnIndex(MessageTable.authoredDate))).getTime()));\n        } catch (ParseException e) {\n            holder.authoredView.setText(\"\");\n            e.printStackTrace();\n        }\n    }\n\n    @Override\n    protected void onContentChanged() {\n        Log.i(TAG, \"onContentChanged\");\n        changeCursor(mDataStore.getRecentMessages().getCursor());\n        mHost.smoothScrollToPosition(0);\n    }\n\n    @Override\n    public ViewHolder onCreateViewHolder(ViewGroup parent, int i) {\n        View v = LayoutInflater.from(parent.getContext())\n                .inflate(R.layout.message_item, parent, false);\n\n        v.setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                if (mListener != null)\n                    mListener.onMessageSelected(v.findViewById(R.id.identicon),\n                                                v.findViewById(R.id.sender),\n                                                (Integer) v.getTag(R.id.view_tag_msg_id),\n                                                (Integer) v.getTag(R.id.view_tag_peer_id));\n            }\n        });\n        // set the view's size, margins, paddings and layout parameters\n        return new ViewHolder(v);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/PeerAdapter.java",
    "content": "package pro.dbro.ble.ui.adapter;\n\nimport android.content.Context;\nimport android.support.v7.widget.RecyclerView;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.TextView;\n\nimport java.util.ArrayList;\n\nimport im.delight.android.identicons.SymmetricIdenticon;\nimport pro.dbro.ble.R;\nimport pro.dbro.ble.data.DataStore;\nimport pro.dbro.ble.data.model.Message;\nimport pro.dbro.ble.data.model.Peer;\n\n/**\n * Created by davidbrodsky on 10/12/14.\n */\npublic class PeerAdapter extends RecyclerView.Adapter<PeerAdapter.ViewHolder> {\n    private Context mContext;\n    private ArrayList<Peer> mPeers;\n\n    // Provide a reference to the type of views that you are using\n    // (custom viewholder)\n    public static class ViewHolder extends RecyclerView.ViewHolder {\n        public TextView mTextView;\n        SymmetricIdenticon mIdenticon;\n\n        public ViewHolder(View v) {\n            super(v);\n            mTextView = (TextView) v.findViewById(R.id.username);\n            mIdenticon = (SymmetricIdenticon) v.findViewById(R.id.identicon);\n        }\n    }\n\n    // Provide a suitable constructor (depends on the kind of dataset)\n    public PeerAdapter(Context context, ArrayList<Peer> peers) {\n        mPeers = peers;\n        mContext = context;\n    }\n\n    // Create new views (invoked by the layout manager)\n    @Override\n    public PeerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,\n                                                   int viewType) {\n        // create a new view\n        View v = LayoutInflater.from(parent.getContext())\n                .inflate(R.layout.peer_item, parent, false);\n        // set the view's size, margins, paddings and layout parameters\n        ViewHolder vh = new ViewHolder(v);\n        return vh;\n    }\n\n    // Replace the contents of a view (invoked by the layout manager)\n    @Override\n    public void onBindViewHolder(ViewHolder holder, int position) {\n        // - get element from your dataset at this position\n        // - replace the contents of the view with that element\n        Peer peer = mPeers.get(position);\n        holder.mTextView.setText(peer.getAlias());\n        holder.mIdenticon.show(new String(peer.getPublicKey()));\n    }\n\n    // Return the size of your dataset (invoked by the layout manager)\n    @Override\n    public int getItemCount() {\n        return mPeers.size();\n    }\n\n    public void notifyPeerAdded(Peer peer) {\n        mPeers.add(peer);\n        notifyItemInserted(mPeers.size()-1);\n    }\n\n    public void notifyPeerRemoved(Peer peer) {\n        int idx = mPeers.indexOf(peer);\n        if (idx != -1) {\n            mPeers.remove(idx);\n            notifyItemRemoved(idx);\n        }\n    }\n\n    public void clearPeers() {\n        mPeers.clear();\n        notifyDataSetChanged();\n    }\n\n    public void notifyMessageReceived(DataStore manager, Message message) {\n        Peer peer = message.getSender(manager);\n        if (peer != null) {\n            int oldIdx = mPeers.indexOf(peer);\n            if (oldIdx != -1 ) {\n                mPeers.remove(peer);\n                mPeers.add(0, peer);\n                notifyItemMoved(oldIdx, 0);\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/RecyclerViewCursorAdapter.java",
    "content": "package pro.dbro.ble.ui.adapter;\n\n/*\n * Copyright (C) 2013 The Android Open Source Project\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 */\n\n/*\n * Copyright (C) 2014 flzyup@ligux.com\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 *\n */\nimport android.content.Context;\nimport android.database.ContentObserver;\nimport android.database.Cursor;\nimport android.database.DataSetObserver;\nimport android.os.Handler;\nimport android.support.v7.widget.RecyclerView;\nimport android.widget.Filter;\nimport android.widget.FilterQueryProvider;\nimport android.widget.Filterable;\n\n/**\n * Version 1.0\n * <p/>\n * Date: 2014-07-07 19:53\n * Author: flzyup@ligux.com\n * <p/>\n * Copyright © 2009-2014 LiGux.com.\n */\npublic abstract class RecyclerViewCursorAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> implements Filterable,\n        CursorFilter.CursorFilterClient {\n\n    /**\n     * Call when bind view with the cursor\n     *\n     * @param holder\n     * @param cursor\n     */\n    public abstract void onBindViewHolder(VH holder, Cursor cursor);\n\n    /**\n     * This field should be made private, so it is hidden from the SDK.\n     * {@hide}\n     */\n    protected boolean mDataValid;\n\n    /**\n     * The current cursor\n     */\n    protected Cursor mCursor;\n\n    /**\n     * This field should be made private, so it is hidden from the SDK.\n     * {@hide}\n     */\n    protected Context mContext;\n\n    /**\n     * The row id column\n     */\n    protected int mRowIDColumn;\n\n    /**\n     * This field should be made private, so it is hidden from the SDK.\n     * {@hide}\n     */\n    protected ChangeObserver mChangeObserver;\n    /**\n     * This field should be made private, so it is hidden from the SDK.\n     * {@hide}\n     */\n    protected DataSetObserver mDataSetObserver;\n\n    /**\n     * This field should be made private, so it is hidden from the SDK.\n     * {@hide}\n     */\n    protected CursorFilter mCursorFilter;\n\n    /**\n     * This field should be made private, so it is hidden from the SDK.\n     * {@hide}\n     */\n    protected FilterQueryProvider mFilterQueryProvider;\n\n    /**\n     * If set the adapter will register a content observer on the cursor and will call\n     * {@link #onContentChanged()} when a notification comes in.  Be careful when\n     * using this flag: you will need to unset the current Cursor from the adapter\n     * to avoid leaks due to its registered observers.  This flag is not needed\n     * when using a CursorAdapter with a\n     * {@link android.content.CursorLoader}.\n     */\n    public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;\n\n    /**\n     * Recommended constructor.\n     *\n     * @param c       The cursor from which to get the data.\n     * @param context The context\n     * @param flags   Flags used to determine the behavior of the adapter;\n     *                Currently it accept {@link #FLAG_REGISTER_CONTENT_OBSERVER}.\n     */\n    public RecyclerViewCursorAdapter(Context context, Cursor c, int flags) {\n        init(context, c, flags);\n    }\n\n    void init(Context context, Cursor c, int flags) {\n\n        boolean cursorPresent = c != null;\n        mCursor = c;\n        mDataValid = cursorPresent;\n        mContext = context;\n        mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow(\"_id\") : -1;\n        if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {\n            mChangeObserver = new ChangeObserver();\n            mDataSetObserver = new MyDataSetObserver();\n        } else {\n            mChangeObserver = null;\n            mDataSetObserver = null;\n        }\n\n        if (cursorPresent) {\n            if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);\n            if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);\n        }\n        setHasStableIds(true);\n    }\n\n    /**\n     * Returns the cursor.\n     *\n     * @return the cursor.\n     */\n    @Override\n    public Cursor getCursor() {\n        return mCursor;\n    }\n\n    /**\n     * @see android.support.v7.widget.RecyclerView.Adapter#getItemCount()\n     */\n    @Override\n    public int getItemCount() {\n        if (mDataValid && mCursor != null) {\n            return mCursor.getCount();\n        } else {\n            return 0;\n        }\n    }\n\n    /**\n     * @param position Adapter position to query\n     * @return\n     * @see android.support.v7.widget.RecyclerView.Adapter#getItemId(int)\n     */\n    @Override\n    public long getItemId(int position) {\n        if (mDataValid && mCursor != null) {\n            if (mCursor.moveToPosition(position)) {\n                return mCursor.getLong(mRowIDColumn);\n            } else {\n                return 0;\n            }\n        } else {\n            return 0;\n        }\n    }\n\n    @Override\n    public void onBindViewHolder(VH holder, int position) {\n        if (!mDataValid) {\n            throw new IllegalStateException(\"this should only be called when the cursor is valid\");\n        }\n        if (!mCursor.moveToPosition(position)) {\n            throw new IllegalStateException(\"couldn't move cursor to position \" + position);\n        }\n        onBindViewHolder(holder, mCursor);\n    }\n\n    /**\n     * Change the underlying cursor to a new cursor. If there is an existing cursor it will be\n     * closed.\n     *\n     * @param cursor The new cursor to be used\n     */\n    public void changeCursor(Cursor cursor) {\n        Cursor old = swapCursor(cursor);\n        if (old != null) {\n            old.close();\n        }\n    }\n\n    /**\n     * Swap in a new Cursor, returning the old Cursor.  Unlike\n     * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>\n     * closed.\n     *\n     * @param newCursor The new cursor to be used.\n     * @return Returns the previously set Cursor, or null if there wasa not one.\n     * If the given new Cursor is the same instance is the previously set\n     * Cursor, null is also returned.\n     */\n    public Cursor swapCursor(Cursor newCursor) {\n        if (newCursor == mCursor) {\n            return null;\n        }\n        Cursor oldCursor = mCursor;\n        if (oldCursor != null) {\n            if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);\n            if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);\n        }\n        mCursor = newCursor;\n        if (newCursor != null) {\n            if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);\n            if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);\n            mRowIDColumn = newCursor.getColumnIndexOrThrow(\"_id\");\n            mDataValid = true;\n            // notify the observers about the new cursor\n            notifyDataSetChanged();\n        } else {\n            mRowIDColumn = -1;\n            mDataValid = false;\n            // notify the observers about the lack of a data set\n            notifyDataSetChanged();\n//            notifyDataSetInvalidated();\n        }\n        return oldCursor;\n    }\n\n    /**\n     * <p>Converts the cursor into a CharSequence. Subclasses should override this\n     * method to convert their results. The default implementation returns an\n     * empty String for null values or the default String representation of\n     * the value.</p>\n     *\n     * @param cursor the cursor to convert to a CharSequence\n     * @return a CharSequence representing the value\n     */\n    public CharSequence convertToString(Cursor cursor) {\n        return cursor == null ? \"\" : cursor.toString();\n    }\n\n    /**\n     * Runs a query with the specified constraint. This query is requested\n     * by the filter attached to this adapter.\n     * <p/>\n     * The query is provided by a\n     * {@link android.widget.FilterQueryProvider}.\n     * If no provider is specified, the current cursor is not filtered and returned.\n     * <p/>\n     * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}\n     * and the previous cursor is closed.\n     * <p/>\n     * This method is always executed on a background thread, not on the\n     * application's main thread (or UI thread.)\n     * <p/>\n     * Contract: when constraint is null or empty, the original results,\n     * prior to any filtering, must be returned.\n     *\n     * @param constraint the constraint with which the query must be filtered\n     * @return a Cursor representing the results of the new query\n     * @see #getFilter()\n     * @see #getFilterQueryProvider()\n     * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)\n     */\n    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {\n        if (mFilterQueryProvider != null) {\n            return mFilterQueryProvider.runQuery(constraint);\n        }\n\n        return mCursor;\n    }\n\n    public Filter getFilter() {\n        if (mCursorFilter == null) {\n            mCursorFilter = new CursorFilter(this);\n        }\n        return mCursorFilter;\n    }\n\n    /**\n     * Returns the query filter provider used for filtering. When the\n     * provider is null, no filtering occurs.\n     *\n     * @return the current filter query provider or null if it does not exist\n     * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)\n     * @see #runQueryOnBackgroundThread(CharSequence)\n     */\n    public FilterQueryProvider getFilterQueryProvider() {\n        return mFilterQueryProvider;\n    }\n\n    /**\n     * Sets the query filter provider used to filter the current Cursor.\n     * The provider's\n     * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}\n     * method is invoked when filtering is requested by a client of\n     * this adapter.\n     *\n     * @param filterQueryProvider the filter query provider or null to remove it\n     * @see #getFilterQueryProvider()\n     * @see #runQueryOnBackgroundThread(CharSequence)\n     */\n    public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {\n        mFilterQueryProvider = filterQueryProvider;\n    }\n\n    /**\n     * Called when the {@link ContentObserver} on the cursor receives a change notification.\n     * The default implementation provides the auto-requery logic, but may be overridden by\n     * sub classes.\n     *\n     * @see ContentObserver#onChange(boolean)\n     */\n    protected abstract void onContentChanged();\n\n    private class ChangeObserver extends ContentObserver {\n        public ChangeObserver() {\n            super(new Handler());\n        }\n\n        @Override\n        public boolean deliverSelfNotifications() {\n            return true;\n        }\n\n        @Override\n        public void onChange(boolean selfChange) {\n            onContentChanged();\n        }\n    }\n\n    private class MyDataSetObserver extends DataSetObserver {\n        @Override\n        public void onChanged() {\n            mDataValid = true;\n            notifyDataSetChanged();\n        }\n\n        @Override\n        public void onInvalidated() {\n            mDataValid = false;\n            notifyDataSetChanged();\n//            notifyDataSetInvalidated();\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/adapter/StatusArrayAdapter.java",
    "content": "package pro.dbro.ble.ui.adapter;\n\nimport android.content.Context;\nimport android.util.DisplayMetrics;\nimport android.util.TypedValue;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.ArrayAdapter;\nimport android.widget.TextView;\n\nimport java.util.ArrayList;\n\nimport pro.dbro.ble.R;\nimport timber.log.Timber;\n\n/**\n * Created by davidbrodsky on 4/20/15.\n */\npublic class StatusArrayAdapter extends ArrayAdapter<String> {\n\n    public StatusArrayAdapter(Context context, ArrayList<String> statuses) {\n        super(context, android.R.layout.simple_spinner_dropdown_item, statuses);\n    }\n\n    @Override\n    public View getDropDownView(int position, View convertView, ViewGroup parent) {\n        return getCustomView(position, convertView, parent);\n    }\n\n    @Override\n    public View getView(int position, View convertView, ViewGroup parent) {\n        return getCustomView(position, convertView, parent);\n    }\n\n    public View getCustomView(int position, View convertView, ViewGroup parent) {\n\n        Context context = parent.getContext();\n\n        // Get the data item for this position\n        String status = getItem(position);\n        // Check if an existing view is being reused, otherwise inflate the view\n        if (convertView == null) {\n            convertView = LayoutInflater.from(getContext()).inflate(android.R.layout.simple_spinner_dropdown_item, parent, false);\n            ((TextView) convertView).setCompoundDrawablePadding((int) dipToPixels(context, 8));\n        }\n\n        TextView statusLabel = (TextView) convertView;\n        statusLabel.setText(status);\n\n        String[] choices = context.getResources().getStringArray(R.array.status_options);\n        if (status.equals(choices[0])) { // Always online\n            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_always_online), null, null, null);\n        }\n        else if (status.equals(choices[1])) { // Online when using app\n            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_online_in_foreground), null, null, null);\n        } else if (status.equals(choices[2])) { // Offline\n            statusLabel.setCompoundDrawablesWithIntrinsicBounds(context.getDrawable(R.drawable.status_offline), null, null, null);\n        } else {\n            Timber.e(\"Unknown status. Cannot set adapter view correctly\");\n            statusLabel.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);\n        }\n\n        return convertView;\n    }\n\n    public static float dipToPixels(Context context, float dipValue) {\n        DisplayMetrics metrics = context.getResources().getDisplayMetrics();\n        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/fragment/MessagingFragment.java",
    "content": "package pro.dbro.ble.ui.fragment;\n\n\nimport android.animation.ObjectAnimator;\nimport android.app.Activity;\nimport android.os.Bundle;\nimport android.support.v4.app.Fragment;\nimport android.support.v7.widget.LinearLayoutManager;\nimport android.support.v7.widget.RecyclerView;\nimport android.util.Log;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.inputmethod.EditorInfo;\nimport android.widget.EditText;\nimport android.widget.TextView;\n\nimport pro.dbro.ble.R;\nimport pro.dbro.ble.data.DataStore;\nimport pro.dbro.ble.ui.adapter.MessageAdapter;\n\n/**\n * A Fragment that currently allows chatting only in the public broadcast mode\n * ala Twitter.\n */\npublic class MessagingFragment extends Fragment implements MessageAdapter.MessageSelectedListener {\n    public static final String TAG = \"MessageListFragment\";\n\n    public static interface ChatFragmentCallback {\n        public void onMessageSendRequested(String message);\n        public void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId);\n    }\n\n    private ChatFragmentCallback mCallback;\n    DataStore mDataStore;\n    RecyclerView mRecyclerView;\n    MessageAdapter mAdapter;\n    EditText mMessageEntry;\n    View mRoot;\n\n    public MessagingFragment() {\n        // Required empty public constructor\n    }\n\n    public void setDataStore(DataStore dataStore) {\n        mDataStore = dataStore;\n    }\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n                             Bundle savedInstanceState) {\n\n        if (mDataStore == null)\n            throw new IllegalStateException(\"MessageListFragment must be equipped with a DataStore. Did you call #setDataStore\");\n\n        // Inflate the layout for this fragment\n        mRoot = inflater.inflate(R.layout.fragment_message, container, false);\n        mMessageEntry = (EditText) mRoot.findViewById(R.id.messageEntry);\n        mMessageEntry.setOnEditorActionListener(new TextView.OnEditorActionListener() {\n            @Override\n            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {\n                if (actionId == EditorInfo.IME_ACTION_SEND) {\n                    sendMessage(v.getText().toString());\n                    v.setText(\"\");\n                    return true;\n                }\n                return false;\n            }\n        });\n        mRoot.findViewById(R.id.sendMessageButton).setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n                onSendMessageButtonClick(v);\n            }\n        });\n        mRecyclerView = (RecyclerView) mRoot.findViewById(R.id.recyclerView);\n        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));\n        mAdapter = new MessageAdapter(getActivity(), null, mDataStore, this, MessageAdapter.FLAG_REGISTER_CONTENT_OBSERVER);\n        mRecyclerView.setAdapter(mAdapter);\n        return mRoot;\n    }\n\n    @Override\n    public void onAttach(Activity activity) {\n        super.onAttach(activity);\n        try {\n            mCallback = (ChatFragmentCallback) activity;\n        } catch (ClassCastException e) {\n            throw new ClassCastException(activity.toString()\n                    + \" must implement ChatFragmentCallback\");\n        }\n    }\n\n    public void onSendMessageButtonClick(View v) {\n        sendMessage(mMessageEntry.getText().toString());\n        mMessageEntry.setText(\"\");\n    }\n\n    private void sendMessage(String message) {\n        if (message.length() == 0) return;\n        Log.i(TAG, \"Sending message \" + message);\n        // For now treat all messsages as public broadcast\n        mCallback.onMessageSendRequested(message);\n\n    }\n\n    @Override\n    public void onMessageSelected(View identiconView, View usernameView, int messageId, int peerId) {\n        mCallback.onMessageSelected(identiconView, usernameView, messageId, peerId);\n    }\n\n    public void animateIn() {\n        mRoot.setAlpha(0);\n        ObjectAnimator animator = ObjectAnimator.ofFloat(mRoot, \"alpha\", 0f, 1f)\n                .setDuration(300);\n\n        animator.setStartDelay(550);\n        animator.start();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/fragment/ProfileFragment.java",
    "content": "package pro.dbro.ble.ui.fragment;\n\n\nimport android.os.Bundle;\nimport android.support.annotation.NonNull;\nimport android.support.v4.app.Fragment;\nimport android.support.v7.widget.LinearLayoutManager;\nimport android.support.v7.widget.RecyclerView;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\n\nimport pro.dbro.ble.R;\nimport pro.dbro.ble.data.DataStore;\nimport pro.dbro.ble.data.model.Peer;\nimport pro.dbro.ble.ui.adapter.MessageAdapter;\n\n/**\n * A Fragment that displays all messages from a particular peer\n */\npublic class ProfileFragment extends Fragment {\n\n    DataStore mDataStore;\n    RecyclerView mRecyclerView;\n    MessageAdapter mAdapter;\n    Peer mFromPeer;\n\n//    TextView mUsernameView;\n\n    public static ProfileFragment createForPeer(@NonNull DataStore dataStore,\n                                                @NonNull Peer peer) {\n\n        ProfileFragment frag = new ProfileFragment();\n        frag.setFromPeer(peer);\n        frag.setDataStore(dataStore);\n        return frag;\n    }\n\n    public ProfileFragment() {\n        // Required empty public constructor\n    }\n\n    public void setFromPeer(Peer fromPeer) {\n        mFromPeer = fromPeer;\n    }\n\n    public void setDataStore(DataStore dataStore) {\n        mDataStore = dataStore;\n    }\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n                             Bundle savedInstanceState) {\n\n        if (mDataStore == null)\n            throw new IllegalStateException(\"MessageListFragment must be equipped with a DataStore. Did you call #setDataStore\");\n\n        // Inflate the layout for this fragment\n        final View root = inflater.inflate(R.layout.fragment_peer_profile, container, false);\n        mRecyclerView = (RecyclerView) root.findViewById(R.id.recyclerView);\n        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));\n        mAdapter = new MessageAdapter(getActivity(), mFromPeer, mDataStore, null, MessageAdapter.FLAG_REGISTER_CONTENT_OBSERVER);\n        mRecyclerView.setAdapter(mAdapter);\n\n//        SymmetricIdenticon identicon = (SymmetricIdenticon) root.findViewById(R.id.profile_identicon);\n//        ((SymmetricIdenticon) root.findViewById(R.id.profile_identicon)).show(new String(mFromPeer.getPublicKey()));\n//        mUsernameView = ((TextView) root.findViewById(R.id.profile_name));\n//        mUsernameView.setText(mFromPeer.getAlias());\n\n        return root;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/pro/dbro/ble/ui/fragment/WelcomeFragment.java",
    "content": "package pro.dbro.ble.ui.fragment;\n\n\nimport android.app.Activity;\nimport android.os.Bundle;\nimport android.support.v4.app.Fragment;\nimport android.view.KeyEvent;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.EditText;\nimport android.widget.TextView;\n\nimport pro.dbro.ble.R;\n\npublic class WelcomeFragment extends Fragment {\n\n    public interface WelcomeFragmentCallback {\n        public void onNameChosen(String name);\n    }\n\n    private WelcomeFragmentCallback mCallback;\n\n    public WelcomeFragment() {\n        // Required empty public constructor\n    }\n\n    @Override\n    public void onAttach(Activity activity) {\n        super.onAttach(activity);\n        try {\n            mCallback = (WelcomeFragmentCallback) activity;\n        } catch (ClassCastException e) {\n            throw new ClassCastException(activity.toString() + \" must implement WelcomeFragmentCallback\");\n        }\n    }\n\n    @Override\n    public void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n    }\n\n    @Override\n    public View onCreateView(LayoutInflater inflater, ViewGroup container,\n                             Bundle savedInstanceState) {\n        View root = inflater.inflate(R.layout.fragment_welcome, container, false);\n        ((EditText) root.findViewById(R.id.aliasEntry)).setOnEditorActionListener(new TextView.OnEditorActionListener() {\n            @Override\n            public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {\n                mCallback.onNameChosen(textView.getText().toString());\n                return false;\n            }\n        });\n        return root;\n    }\n\n\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/status_always_online.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n\n    <solid\n        android:color=\"@color/status_always_online\"/>\n\n    <size\n        android:width=\"20dp\"\n        android:height=\"20dp\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/status_offline.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n\n    <solid\n        android:color=\"@color/status_offline\"/>\n\n    <size\n        android:width=\"20dp\"\n        android:height=\"20dp\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/status_online_in_foreground.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n\n    <solid\n        android:color=\"@color/status_online_in_foreground\"/>\n\n    <size\n        android:width=\"20dp\"\n        android:height=\"20dp\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/transparent_button.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ripple xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:color=\"@color/primary\">\n    <item>\n        <shape android:shape=\"rectangle\">\n            <solid android:color=\"@color/messageEntryBackground\" />\n        </shape>\n    </item>\n</ripple>"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<android.support.v4.widget.DrawerLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/my_drawer_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:fitsSystemWindows=\"true\">\n\n    <!-- Your normal content view -->\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <!-- We use a Toolbar so that our drawer can be displayed\n             in front of the action bar -->\n        <android.support.v7.widget.Toolbar\n            android:id=\"@+id/toolbar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"50dp\"\n            android:animateLayoutChanges=\"true\"\n            android:background=\"@color/primary\"/>\n        <!-- on API 21 background will be ?attr/colorPrimary -->\n        <!-- on API 21 minHeight will be ?attr/actionBarSize-->\n\n        <!-- The rest of your content view -->\n\n        <FrameLayout\n            android:id=\"@+id/container\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            tools:context=\"pro.dbro.ble.ui.activities.MainActivity\"\n            tools:ignore=\"MergeRootFrame\" />\n\n    </LinearLayout>\n\n    <!-- Your drawer view. This can be any view, LinearLayout\n         is just an example. As we have set fitSystemWindows=true\n         this will be displayed under the status bar. -->\n    <com.google.samples.apps.iosched.ui.widget.ScrimInsetsScrollView\n        android:layout_width=\"304dp\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"start\"\n        android:background=\"#ffd4d8dd\"\n        android:fitsSystemWindows=\"true\"\n        app:insetForeground=\"#4000\">\n\n        <!-- Your drawer content -->\n\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <RelativeLayout\n                android:id=\"@+id/profile_header\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_alignParentStart=\"true\"\n                android:layout_alignParentTop=\"true\"\n                android:background=\"#ffbdccdd\"\n                android:gravity=\"center_vertical\"\n                android:orientation=\"horizontal\"\n                android:paddingBottom=\"8dp\"\n                android:paddingEnd=\"16dp\"\n                android:paddingStart=\"16dp\"\n                android:paddingTop=\"35dp\">\n\n                <im.delight.android.identicons.SymmetricIdenticon\n                    android:id=\"@+id/profile_identicon\"\n                    android:layout_width=\"45dp\"\n                    android:layout_height=\"45dp\"\n                    android:layout_alignParentStart=\"true\"\n                    android:layout_alignParentTop=\"true\" />\n\n                <TextView\n                    android:id=\"@+id/profile_name\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_marginStart=\"16dp\"\n                    android:layout_toEndOf=\"@id/profile_identicon\"\n                    android:layout_alignTop=\"@id/profile_identicon\"\n                    android:layout_alignBottom=\"@id/profile_identicon\"\n                    android:gravity=\"center_vertical\"\n                    android:textSize=\"20sp\"\n                    android:textStyle=\"bold\" />\n\n                <View\n                    android:id=\"@+id/divider\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"1dp\"\n                    android:layout_below=\"@id/profile_identicon\"\n                    android:layout_marginBottom=\"8dp\"\n                    android:layout_marginTop=\"8dp\"\n                    android:background=\"#2e615f5b\" />\n\n                <LinearLayout\n                    android:layout_below=\"@id/divider\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_centerHorizontal=\"true\"\n                    android:orientation=\"horizontal\">\n\n                    <LinearLayout\n                        android:id=\"@+id/msg_pass_container\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_gravity=\"center_horizontal\"\n                        android:gravity=\"center\"\n                        android:orientation=\"vertical\">\n\n                        <TextView\n                            android:id=\"@+id/msg_pass_count\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:textSize=\"24sp\" />\n\n                        <TextView\n                            android:id=\"@+id/msg_pass_count_label\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:text=\"@string/messages_passed\"\n                            android:textSize=\"12sp\" />\n\n                    </LinearLayout>\n\n                    <LinearLayout\n                        android:id=\"@+id/peers_met_container\"\n                        android:layout_width=\"wrap_content\"\n                        android:layout_height=\"wrap_content\"\n                        android:layout_below=\"@id/divider\"\n                        android:layout_gravity=\"center_horizontal\"\n                        android:layout_marginStart=\"16dp\"\n                        android:layout_toEndOf=\"@id/msg_pass_container\"\n                        android:gravity=\"center\"\n                        android:orientation=\"vertical\">\n\n                        <TextView\n                            android:id=\"@+id/peers_met_count\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:textSize=\"24sp\" />\n\n                        <TextView\n                            android:id=\"@+id/peers_met_count_label\"\n                            android:layout_width=\"wrap_content\"\n                            android:layout_height=\"wrap_content\"\n                            android:text=\"@string/peers_known\"\n                            android:textSize=\"12sp\"/>\n\n                    </LinearLayout>\n\n                </LinearLayout>\n\n            </RelativeLayout>\n\n            <Spinner\n                android:id=\"@+id/status_spinner\"\n                android:layout_width=\"fill_parent\"\n                android:layout_height=\"48dp\"\n                android:layout_below=\"@id/profile_header\"\n                android:layout_marginEnd=\"16dp\"\n                android:layout_marginTop=\"8dp\"\n                android:paddingStart=\"16dp\"\n                android:textSize=\"18sp\" />\n\n            <TextView\n                android:id=\"@+id/log\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:layout_below=\"@id/status_spinner\"\n                android:padding=\"8dp\"\n                android:textColor=\"#ffffff\" />\n\n        </RelativeLayout>\n\n    </com.google.samples.apps.iosched.ui.widget.ScrimInsetsScrollView>\n\n</android.support.v4.widget.DrawerLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/dialog_welcome.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:padding=\"16dp\">\n\n    <EditText\n        android:id=\"@+id/aliasEntry\"\n        android:drawableStart=\"@drawable/ic_user\"\n        android:background=\"@android:color/transparent\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:textSize=\"22sp\"\n        android:maxLength=\"@integer/max_alias_length\"\n        android:layout_centerInParent=\"true\"\n        android:imeOptions=\"actionDone\"\n        android:singleLine=\"true\"\n        android:hint=\"@string/dialog_welcome_user_alias_hint\"/>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/fragment_message.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"pro.dbro.ble.ui.fragment.MessagingFragment\">\n\n    <RelativeLayout\n        android:id=\"@+id/messageEntryContainer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@color/messageEntryBackground\"\n        android:layout_alignParentBottom=\"true\" >\n\n        <Button\n            android:id=\"@+id/sendMessageButton\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:background=\"@drawable/transparent_button\"\n            android:text=\"@string/send\"\n            android:padding=\"8dp\"\n            android:gravity=\"center\"\n            android:layout_alignParentEnd=\"true\"/>\n\n        <EditText\n            android:id=\"@+id/messageEntry\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:singleLine=\"true\"\n            android:maxLength=\"@integer/max_msg_length\"\n            android:imeOptions=\"actionSend\"\n            android:paddingLeft=\"16dp\"\n            android:layout_alignTop=\"@id/sendMessageButton\"\n            android:layout_alignBottom=\"@id/sendMessageButton\"\n            android:hint=\"@string/message_entry_hint\"\n            android:background=\"@color/messageEntryBackground\"\n            android:layout_toStartOf=\"@id/sendMessageButton\"/>\n\n    </RelativeLayout>\n\n    <android.support.v7.widget.RecyclerView\n        android:id=\"@+id/recyclerView\"\n        android:background=\"@color/primary_subdued\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_above=\"@id/messageEntryContainer\"/>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_peer.xml",
    "content": "<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"pro.dbro.ble.ui.fragment.PeerListFragment\">\n\n    <android.support.v7.widget.RecyclerView\n        android:id=\"@+id/recyclerView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" />\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_peer_profile.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"pro.dbro.ble.ui.fragment.MessagingFragment\">\n\n    <android.support.v7.widget.RecyclerView\n        android:id=\"@+id/recyclerView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"/>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/fragment_welcome.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"#407743\" >\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <ImageView\n            android:id=\"@+id/image\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentTop=\"true\"\n            android:adjustViewBounds=\"true\"\n            android:scaleType=\"centerCrop\"\n            android:src=\"@drawable/sneakernet\" />\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginTop=\"8dp\"\n            android:text=\"Sneakernet\"\n            android:textColor=\"@android:color/white\"\n            android:textSize=\"30sp\" />\n\n        <TextView\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:text=\"offline messenger\"\n            android:textColor=\"#96ffffff\"\n            android:textSize=\"20sp\" />\n\n        <TextView\n            android:id=\"@+id/body\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"center_horizontal\"\n            android:layout_marginEnd=\"8dp\"\n            android:layout_marginStart=\"8dp\"\n            android:layout_marginTop=\"16dp\"\n            android:text=\"This is a messaging experiment powered by sneakers. Messages are exchanged with others you pass throughout the day, forming a network of the people around you.\"\n            android:textColor=\"#96ffffff\"\n            android:textSize=\"20sp\" />\n\n        <RelativeLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginBottom=\"24dp\">\n\n            <TextView\n                android:id=\"@+id/call_me\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginEnd=\"8dp\"\n                android:layout_marginStart=\"8dp\"\n                android:layout_marginTop=\"8dp\"\n                android:text=\"Call me\"\n                android:textColor=\"#96ffffff\"\n                android:textSize=\"20sp\"\n                android:layout_alignParentStart=\"true\"/>\n\n            <EditText\n                android:id=\"@+id/aliasEntry\"\n                android:layout_width=\"wrap_content\"\n                android:layout_height=\"wrap_content\"\n                android:textSize=\"22sp\"\n                android:maxLength=\"@integer/max_alias_length\"\n                android:imeOptions=\"actionDone\"\n                android:singleLine=\"true\"\n                android:textColor=\"#ffffff\"\n                android:textColorHint=\"#b6ffffff\"\n                android:layout_toEndOf=\"@id/call_me\"\n                android:layout_alignBaseline=\"@id/call_me\"\n                android:hint=\"@string/dialog_welcome_user_alias_hint\"/>\n\n        </RelativeLayout>\n\n    </LinearLayout>\n</ScrollView>"
  },
  {
    "path": "app/src/main/res/layout/message_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.v7.widget.CardView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:card_view=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/card_view\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"center\"\n    android:layout_marginStart=\"8dp\"\n    android:layout_marginEnd=\"8dp\"\n    android:layout_marginTop=\"4dp\"\n    android:layout_marginBottom=\"4dp\"\n    android:clickable=\"true\"\n    android:foreground=\"?android:attr/selectableItemBackground\"\n    card_view:cardCornerRadius=\"4dp\">\n\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\">\n\n        <im.delight.android.identicons.SymmetricIdenticon\n            android:id=\"@+id/identicon\"\n            android:layout_alignParentStart=\"true\"\n            android:layout_centerVertical=\"true\"\n            android:layout_margin=\"8dp\"\n            android:layout_width=\"48dp\"\n            android:layout_height=\"48dp\" />\n\n        <TextView\n            android:id=\"@+id/sender\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_toEndOf=\"@id/identicon\"\n            android:layout_alignParentTop=\"true\"\n            android:ellipsize=\"marquee\"\n            android:gravity=\"center_vertical\"\n            android:paddingEnd=\"8dp\"\n            android:singleLine=\"true\"\n            android:paddingTop=\"2dp\"\n            android:paddingBottom=\"2dp\"\n            android:textStyle=\"bold\"\n            android:text=\"Paulina Pom pom\"\n            android:textColor=\"#ff474747\"\n            android:textSize=\"14sp\" />\n\n        <TextView\n            android:id=\"@+id/authoredDate\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:gravity=\"center_vertical|end\"\n            android:paddingTop=\"2dp\"\n            android:paddingBottom=\"2dp\"\n            android:paddingEnd=\"8dp\"\n            android:layout_toEndOf=\"@id/sender\"\n            android:textColor=\"#ffbdbdbd\"\n            android:text=\"3 hours ago\"\n            android:textSize=\"14sp\"/>\n\n        <TextView\n            android:id=\"@+id/messageBody\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_toEndOf=\"@id/identicon\"\n            android:layout_alignParentEnd=\"true\"\n            android:layout_below=\"@id/sender\"\n            android:gravity=\"top|start\"\n            android:paddingTop=\"2dp\"\n            android:text=\"What's for dinner?\"\n            android:textSize=\"20sp\" />\n    </RelativeLayout>\n</android.support.v7.widget.CardView>"
  },
  {
    "path": "app/src/main/res/layout/peer_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/container\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:layout_gravity=\"center\"\n    android:layout_margin=\"2dp\"\n    android:orientation=\"vertical\">\n\n    <im.delight.android.identicons.SymmetricIdenticon\n        android:id=\"@+id/identicon\"\n        android:layout_width=\"12dp\"\n        android:layout_height=\"12dp\"\n        android:layout_gravity=\"center_horizontal\"/>\n\n    <TextView\n        android:id=\"@+id/username\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"match_parent\"\n        android:textColor=\"#ffffff\"\n        android:textSize=\"12sp\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/status_item.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/status_label\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:paddingEnd=\"16dp\"\n    android:paddingStart=\"8dp\" />"
  },
  {
    "path": "app/src/main/res/menu/menu_debug.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\".MyActivity\" >\n    <item android:id=\"@+id/action_clear\"\n        android:title=\"@string/clear_logs\"\n        android:orderInCategory=\"100\"\n        android:showAsAction=\"always\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/menu/menu_main.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    tools:context=\"pro.dbro.ble.ui.activities.MainActivity\">\n    <item\n        android:id=\"@+id/action_settings\"\n        android:orderInCategory=\"100\"\n        android:showAsAction=\"never\"\n        android:title=\"@string/action_settings\" />\n</menu>\n"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <declare-styleable name=\"ScrimInsetsView\">\n        <attr name=\"insetForeground\" format=\"reference|color\" />\n    </declare-styleable>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"primary_subdued\">#09307adf</color>\n    <color name=\"primary\">#ff307adf</color>\n    <color name=\"primaryDark\">#ff22569D</color>\n\n    <color name=\"message_header_subdued_text\">#cfd8dc</color>\n    <color name=\"message_header\">#ff8a8c89</color>\n    <!--#bdbdbd\n    <color name=\"dull\">#738ffe</color>\n    -->\n\n    <color name=\"messageEntryBackground\">#ffffff</color>\n\n    <color name=\"status_always_online\">#4CAF50</color>\n    <color name=\"status_online_in_foreground\">#81C784</color>\n    <color name=\"status_offline\">#BDBDBD</color>\n\n    <color name=\"welcome_status_bar\">#26136c</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\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</resources>\n"
  },
  {
    "path": "app/src/main/res/values/ids.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <item type=\"id\" name=\"view_tag_msg_id\"/>\n    <item type=\"id\" name=\"view_tag_peer_id\"/>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/ints.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <integer name=\"max_alias_length\">35</integer>\n    <integer name=\"max_msg_length\">140</integer>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings-machine.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"identicon_transition_name\">identicon</string>\n    <string name=\"username_transition_name\">username</string>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <string name=\"app_name\">Sneakernet</string>\n    <string name=\"action_settings\">Settings</string>\n    <string name=\"bt_unavailable\">Bluetooth unavailable</string>\n    <string name=\"ble_not_supported\">Bluetooth not supported</string>\n    <string name=\"scan_started\">Scan Started</string>\n    <string name=\"scan_stopped\">Scan Stopped</string>\n    <string name=\"stop_scanning\">Stop Scanning</string>\n    <string name=\"start_scanning\">Start Scanning</string>\n    <string name=\"stop_advertising\">Stop Advertising</string>\n    <string name=\"start_advertising\">Start Advertising</string>\n    <string name=\"clear_logs\">Clear logs</string>\n    <string name=\"text_entry_hint\">Message to Send</string>\n    <string name=\"dialog_welcome_greeting\">Hello! Who are you?</string>\n    <string name=\"dialog_welcome_user_alias_hint\">Your Name</string>\n    <string name=\"dialog_ok\">OK</string>\n    <string name=\"title_activity_main\">Nearby</string>\n    <string name=\"hello_blank_fragment\">Hello blank fragment</string>\n    <string name=\"send\">SEND</string>\n    <string name=\"message_entry_hint\">New Public Message</string>\n    <string name=\"notification_new_messages\">New Messages</string>\n    <string name=\"notification_touch_to_chat\">Touch to chat</string>\n    <string name=\"remain_online_in_background\">Remain online in background</string>\n    <string name=\"online\">Online</string>\n    <string name=\"peers_known\">Peers known</string>\n    <string name=\"messages_passed\">Messages passed</string>\n    <string name=\"drawer_open\">Open Drawer</string>\n    <string name=\"drawer_close\">Close Drawer</string>\n    <string name=\"public_feed\">Public Feed</string>\n\n    <string-array name=\"status_options\">\n        <item>Always Online</item>\n        <item>Online when using App</item>\n        <item>Offline</item>\n    </string-array>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <item name=\"android:colorPrimary\">@color/primary</item>\n        <item name=\"android:colorPrimaryDark\">@color/primaryDark</item>\n        <item name=\"android:colorControlHighlight\">@color/primary</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n\n        <item name=\"drawerArrowStyle\">@style/DrawerArrowStyle</item>\n    </style>\n\n    <style name=\"DrawerArrowStyle\" parent=\"Widget.AppCompat.DrawerArrowToggle\">\n        <item name=\"spinBars\">false</item>\n        <item name=\"color\">@android:color/white</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-w820dp/dimens.xml",
    "content": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.xml\n         (such as screen margins) for screens with more than 820dp of available width. This\n         would include 7\" and 10\" devices in landscape (~960dp and ~1280dp respectively). -->\n    <dimen name=\"activity_horizontal_margin\">64dp</dimen>\n</resources>\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    repositories {\n        jcenter()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:1.2.3'\n        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.2'\n        classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n        jcenter()\n    }\n}\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Wed Dec 10 14:12:56 PST 2014\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-2.2.1-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Settings specified in this file will override any Gradle settings\n# configured through the IDE.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx10248m -XX:MaxPermSize=256m\n# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched.\nif $cygwin ; then\n    [ -n \"$JAVA_HOME\" ] && JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\nfi\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >&-\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >&-\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windowz variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\ngoto execute\r\n\r\n:4NT_args\r\n@rem Get arguments from the 4NT Shell from JP Software\r\nset CMD_LINE_ARGS=%$\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "pull_on_app_database.sh",
    "content": "#!/bin/bash\n\n# Pull the database for this application to the Desktop\n\nadb backup -f ~/Desktop/ble.ab -noapk pro.dbro.ble\ndd if=~/Desktop/ble.ab bs=1 skip=24 | python -c \"import zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))\" | tar -xvf -"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app'\ninclude ':submodules:airshare:sdk'\n"
  }
]